/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.util;
import hudson.EnvVars;
import hudson.Functions;
import hudson.Launcher;
import hudson.ProxyConfiguration;
import hudson.RelativePath;
import hudson.Util;
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.tasks.Builder;
import hudson.util.ReflectionUtils.Parameter;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.Stapler;
import org.springframework.util.StringUtils;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import static hudson.Functions.jsStringEscape;
import static hudson.Util.*;
/**
* Represents the result of the form field validation.
*
* <p>
* Use one of the factory methods to create an instance, then return it from your <tt>doCheckXyz</tt>
* method. (Via {@link HttpResponse}, the returned object will render the result into {@link StaplerResponse}.)
* This way of designing form field validation allows you to reuse {@code doCheckXyz()} methods
* programmatically as well (by using {@link #kind}.
*
* <p>
* For typical validation needs, this class offers a number of {@code validateXXX(...)} methods, such as
* {@link #validateExecutable(String)}. {@link FilePath} also has a number of {@code validateXXX(...)} methods
* that you may be able to reuse.
*
* <p>
* Also see <tt>doCheckCvsRoot</tt> in <tt>CVSSCM</tt> as an example.
*
* <p>
* This class extends {@link IOException} so that it can be thrown from a method. This allows one to reuse
* the checking logic as a part of the real computation, such as:
*
* <pre>
* String getAntVersion(File antHome) throws FormValidation {
* if(!antHome.isDirectory())
* throw FormValidation.error(antHome+" doesn't look like a home directory");
* ...
* return IOUtils.toString(new File(antHome,"version"));
* }
*
* ...
*
* public FormValidation doCheckAntVersion(@QueryParameter String f) {
* try {
* return ok(getAntVersion(new File(f)));
* } catch (FormValidation f) {
* return f;
* }
* }
*
* ...
*
* public void {@linkplain Builder#perform(AbstractBuild, Launcher, BuildListener) perform}(...) {
* String version = getAntVersion(antHome);
* ...
* }
* </pre>
*
* @author Kohsuke Kawaguchi
* @since 1.294
*/
public abstract class FormValidation extends IOException implements HttpResponse {
/**
* Indicates the kind of result.
*/
public enum Kind {
/**
* Form field value was OK and no problem was detected.
*/
OK,
/**
* Form field value contained something suspicious. For some limited use cases
* the value could be valid, but we suspect the user made a mistake.
*/
WARNING,
/**
* Form field value contained a problem that should be corrected.
*/
ERROR
}
/**
* Sends out a string error message that indicates an error.
*
* @param message
* Human readable message to be sent. <tt>error(null)</tt>
* can be used as <tt>ok()</tt>.
*/
public static FormValidation error(String message) {
return errorWithMarkup(message==null?null: Util.escape(message));
}
public static FormValidation warning(String message) {
return warningWithMarkup(message==null?null:Util.escape(message));
}
public static FormValidation ok(String message) {
return okWithMarkup(message==null?null:Util.escape(message));
}
/**
* Singleton instance that represents "OK".
*/
private static final FormValidation OK = respond(Kind.OK,"<div/>");
public static FormValidation ok() {
return OK;
}
/**
* Sends out a string error message that indicates an error,
* by formatting it with {@link String#format(String, Object[])}
*/
public static FormValidation error(String format, Object... args) {
return error(String.format(format,args));
}
public static FormValidation warning(String format, Object... args) {
return warning(String.format(format,args));
}
public static FormValidation ok(String format, Object... args) {
return ok(String.format(format,args));
}
/**
* Sends out a string error message, with optional "show details" link that expands to the full stack trace.
*
* <p>
* Use this with caution, so that anonymous users do not gain too much insights into the state of the system,
* as error stack trace often reveals a lot of information. Consider if a check operation needs to be exposed
* to everyone or just those who have higher access to job/hudson/etc.
*/
public static FormValidation error(Throwable e, String message) {
return _error(Kind.ERROR, e, message);
}
public static FormValidation warning(Throwable e, String message) {
return _error(Kind.WARNING, e, message);
}
private static FormValidation _error(Kind kind, Throwable e, String message) {
if (e==null) return _errorWithMarkup(Util.escape(message),kind);
return _errorWithMarkup(Util.escape(message)+
" <a href='#' class='showDetails'>"
+ Messages.FormValidation_Error_Details()
+ "</a><pre style='display:none'>"
+ Util.escape(Functions.printThrowable(e)) +
"</pre>",kind
);
}
public static FormValidation error(Throwable e, String format, Object... args) {
return error(e,String.format(format,args));
}
public static FormValidation warning(Throwable e, String format, Object... args) {
return warning(e,String.format(format,args));
}
/**
* Aggregate multiple validations into one.
*
* @return Validation of the least successful kind aggregating all child messages.
* @since 1.590
*/
public static @Nonnull FormValidation aggregate(@Nonnull Collection<FormValidation> validations) {
if (validations == null || validations.isEmpty()) return FormValidation.ok();
if (validations.size() == 1) return validations.iterator().next();
final StringBuilder sb = new StringBuilder("<ul style='list-style-type: none; padding-left: 0; margin: 0'>");
FormValidation.Kind worst = Kind.OK;
for (FormValidation validation: validations) {
sb.append("<li>").append(validation.renderHtml()).append("</li>");
if (validation.kind.ordinal() > worst.ordinal()) {
worst = validation.kind;
}
}
sb.append("</ul>");
return respond(worst, sb.toString());
}
/**
* Sends out an HTML fragment that indicates an error.
*
* <p>
* This method must be used with care to avoid cross-site scripting
* attack.
*
* @param message
* Human readable message to be sent. <tt>error(null)</tt>
* can be used as <tt>ok()</tt>.
*/
public static FormValidation errorWithMarkup(String message) {
return _errorWithMarkup(message,Kind.ERROR);
}
public static FormValidation warningWithMarkup(String message) {
return _errorWithMarkup(message,Kind.WARNING);
}
public static FormValidation okWithMarkup(String message) {
return _errorWithMarkup(message,Kind.OK);
}
private static FormValidation _errorWithMarkup(final String message, final Kind kind) {
if(message==null)
return ok();
return new FormValidation(kind, message) {
public String renderHtml() {
StaplerRequest req = Stapler.getCurrentRequest();
if (req == null) { // being called from some other context
return message;
}
// 1x16 spacer needed for IE since it doesn't support min-height
return "<div class="+ kind.name().toLowerCase(Locale.ENGLISH) +"><img src='"+
req.getContextPath()+ Jenkins.RESOURCE_PATH+"/images/none.gif' height=16 width=1>"+
message+"</div>";
}
@Override public String toString() {
return kind + ": " + message;
}
};
}
/**
* Sends out an arbitrary HTML fragment as the output.
*/
public static FormValidation respond(Kind kind, final String html) {
return new FormValidation(kind) {
public String renderHtml() {
return html;
}
@Override public String toString() {
return kind + ": " + html;
}
};
}
/**
* Performs an application-specific validation on the given file.
*
* <p>
* This is used as a piece in a bigger validation effort.
*/
public static abstract class FileValidator {
public abstract FormValidation validate(File f);
/**
* Singleton instance that does no check.
*/
public static final FileValidator NOOP = new FileValidator() {
public FormValidation validate(File f) {
return ok();
}
};
}
/**
* Makes sure that the given string points to an executable file.
*/
public static FormValidation validateExecutable(String exe) {
return validateExecutable(exe, FileValidator.NOOP);
}
/**
* Makes sure that the given string points to an executable file.
*
* @param exeValidator
* If the validation process discovers a valid executable program on the given path,
* the specified {@link FileValidator} can perform additional checks (such as making sure
* that it has the right version, etc.)
*/
public static FormValidation validateExecutable(String exe, FileValidator exeValidator) {
// insufficient permission to perform validation?
if(!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) return ok();
exe = fixEmpty(exe);
if(exe==null)
return ok();
if(exe.indexOf(File.separatorChar)>=0) {
// this is full path
File f = new File(exe);
if(f.exists()) return exeValidator.validate(f);
File fexe = new File(exe+".exe");
if(fexe.exists()) return exeValidator.validate(fexe);
return error("There's no such file: "+exe);
}
// look in PATH
String path = EnvVars.masterEnvVars.get("PATH");
String tokenizedPath = "";
String delimiter = null;
if(path!=null) {
for (String _dir : Util.tokenize(path.replace("\\", "\\\\"),File.pathSeparator)) {
if (delimiter == null) {
delimiter = ", ";
}
else {
tokenizedPath += delimiter;
}
tokenizedPath += _dir.replace('\\', '/');
File dir = new File(_dir);
File f = new File(dir,exe);
if(f.exists()) return exeValidator.validate(f);
File fexe = new File(dir,exe+".exe");
if(fexe.exists()) return exeValidator.validate(fexe);
}
tokenizedPath += ".";
} else {
tokenizedPath = "unavailable.";
}
// didn't find it
return error("There's no such executable "+exe+" in PATH: "+tokenizedPath);
}
/**
* Makes sure that the given string is a non-negative integer.
*/
public static FormValidation validateNonNegativeInteger(String value) {
try {
if(Integer.parseInt(value)<0)
return error(hudson.model.Messages.Hudson_NotANonNegativeNumber());
return ok();
} catch (NumberFormatException e) {
return error(hudson.model.Messages.Hudson_NotANumber());
}
}
/**
* Makes sure that the given string is a positive integer.
*/
public static FormValidation validatePositiveInteger(String value) {
try {
if(Integer.parseInt(value)<=0)
return error(hudson.model.Messages.Hudson_NotAPositiveNumber());
return ok();
} catch (NumberFormatException e) {
return error(hudson.model.Messages.Hudson_NotANumber());
}
}
/**
* Makes sure that the given string is not null or empty.
*/
public static FormValidation validateRequired(String value) {
if (Util.fixEmptyAndTrim(value) == null)
return error(Messages.FormValidation_ValidateRequired());
return ok();
}
/**
* Makes sure that the given string is a base64 encoded text.
*
* @param allowWhitespace
* if you allow whitespace (CR,LF,etc) in base64 encoding
* @param allowEmpty
* Is empty string allowed?
* @param errorMessage
* Error message.
* @since 1.305
*/
public static FormValidation validateBase64(String value, boolean allowWhitespace, boolean allowEmpty, String errorMessage) {
try {
String v = value;
if(!allowWhitespace) {
if(v.indexOf(' ')>=0 || v.indexOf('\n')>=0)
return error(errorMessage);
}
v=v.trim();
if(!allowEmpty && v.length()==0)
return error(errorMessage);
com.trilead.ssh2.crypto.Base64.decode(v.toCharArray());
return ok();
} catch (IOException e) {
return error(errorMessage);
}
}
/**
* Convenient base class for checking the validity of URLs.
*
* <p>
* This allows the check method to call various utility methods in a concise syntax.
*/
public static abstract class URLCheck {
/**
* Opens the given URL and reads text content from it.
* This method honors Content-type header.
*/
protected BufferedReader open(URL url) throws IOException {
// use HTTP content type to find out the charset.
URLConnection con = ProxyConfiguration.open(url);
if (con == null) { // TODO is this even permitted by URL.openConnection?
throw new IOException(url.toExternalForm());
}
return new BufferedReader(
new InputStreamReader(con.getInputStream(),getCharset(con)));
}
/**
* Finds the string literal from the given reader.
* @return
* true if found, false otherwise.
*/
protected boolean findText(BufferedReader in, String literal) throws IOException {
String line;
while((line=in.readLine())!=null)
if(line.indexOf(literal)!=-1)
return true;
return false;
}
/**
* Calls the {@link FormValidation#error(String)} method with a reasonable error message.
* Use this method when the {@link #open(URL)} or {@link #findText(BufferedReader, String)} fails.
*
* @param url
* Pass in the URL that was connected. Used for error diagnosis.
*/
protected FormValidation handleIOException(String url, IOException e) throws IOException, ServletException {
// any invalid URL comes here
if(e.getMessage().equals(url))
// Sun JRE (and probably others too) often return just the URL in the error.
return error("Unable to connect "+url);
else
return error(e.getMessage());
}
/**
* Figures out the charset from the content-type header.
*/
private String getCharset(URLConnection con) {
for( String t : con.getContentType().split(";") ) {
t = t.trim().toLowerCase(Locale.ENGLISH);
if(t.startsWith("charset="))
return t.substring(8);
}
// couldn't find it. HTML spec says default is US-ASCII,
// but UTF-8 is a better choice since
// (1) it's compatible with US-ASCII
// (2) a well-written web applications tend to use UTF-8
return "UTF-8";
}
/**
* Implement the actual form validation logic, by using other convenience methosd defined in this class.
* If you are not using any of those, you don't need to extend from this class.
*/
protected abstract FormValidation check() throws IOException, ServletException;
}
public final Kind kind;
/**
* Instances should be created via one of the factory methods above.
* @param kind
*/
private FormValidation(Kind kind) {
this.kind = kind;
}
private FormValidation(Kind kind, String message) {
super(message);
this.kind = kind;
}
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
respond(rsp, renderHtml());
}
public abstract String renderHtml();
/**
* Sends out an arbitrary HTML fragment as the output.
*/
protected void respond(StaplerResponse rsp, String html) throws IOException, ServletException {
rsp.setContentType("text/html;charset=UTF-8");
rsp.getWriter().print(html);
}
/**
* Builds up the check URL for the client-side JavaScript to call back.
*/
public static class CheckMethod {
private final Descriptor descriptor;
private final Method method;
private final String capitalizedFieldName;
/**
* Names of the parameters to pass from the client.
*/
private final List<String> names;
private volatile String checkUrl; // cached once computed
private volatile String dependsOn; // cached once computed
public CheckMethod(Descriptor descriptor, String fieldName) {
this.descriptor = descriptor;
this.capitalizedFieldName = StringUtils.capitalize(fieldName);
method = ReflectionUtils.getPublicMethodNamed(descriptor.getClass(), "doCheck" + capitalizedFieldName);
if(method !=null) {
names = new ArrayList<String>();
findParameters(method);
} else {
names = null;
}
}
/**
* Builds query parameter line by figuring out what should be submitted
*/
private void findParameters(Method method) {
for (Parameter p : ReflectionUtils.getParameters(method)) {
QueryParameter qp = p.annotation(QueryParameter.class);
if (qp!=null) {
String name = qp.value();
if (name.length()==0) name = p.name();
if (name==null || name.length()==0)
continue; // unknown parameter name. we'll report the error when the form is submitted.
if (name.equals("value"))
continue; // 'value' parameter is implicit
RelativePath rp = p.annotation(RelativePath.class);
if (rp!=null)
name = rp.value()+'/'+name;
names.add(name);
continue;
}
Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
if (m!=null) findParameters(m);
}
}
/**
* Obtains the 1.526-compatible single string representation.
*
* This method computes JavaScript expression, which evaluates to the URL that the client should request
* the validation to.
* A modern version depends on {@link #toStemUrl()} and {@link #getDependsOn()}
*/
public String toCheckUrl() {
if (names==null) return null;
if (checkUrl==null) {
StringBuilder buf = new StringBuilder(singleQuote(relativePath()));
if (!names.isEmpty()) {
buf.append("+qs(this).addThis()");
for (String name : names) {
buf.append(".nearBy('"+name+"')");
}
buf.append(".toString()");
}
checkUrl = buf.toString();
}
// put this under the right contextual umbrella.
// 'a' in getCurrentDescriptorByNameUrl is always non-null because we already have Hudson as the sentinel
return '\'' + jsStringEscape(Descriptor.getCurrentDescriptorByNameUrl()) + "/'+" + checkUrl;
}
/**
* Returns the URL that the JavaScript should hit to perform form validation, except
* the query string portion (which is built on the client side.)
*/
public String toStemUrl() {
if (names==null) return null;
return jsStringEscape(Descriptor.getCurrentDescriptorByNameUrl()) + '/' + relativePath();
}
public String getDependsOn() {
if (names==null) return null;
if (dependsOn==null)
dependsOn = join(names," ");
return dependsOn;
}
private String relativePath() {
return descriptor.getDescriptorUrl() + "/check" + capitalizedFieldName;
}
}
}