/*******************************************************************************
*
* Copyright (c) 2004-2009, Oracle Corporation
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
*
*
*
*******************************************************************************/
package hudson.util;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Functions;
import hudson.ProxyConfiguration;
import hudson.Util;
import static hudson.Util.fixEmpty;
import hudson.model.Hudson;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.Locale;
import javax.servlet.ServletException;
import org.apache.commons.codec.binary.Base64;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
/**
* 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 {@link CVSSCM.DescriptorImpl#doCheckCvsRoot(String)} 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 hudson.tasks.Builder#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.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'>"
+ 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));
}
/**
* 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() {
// 1x16 spacer needed for IE since it doesn't support min-height
return "<div class=" + kind.name().toLowerCase(Locale.ENGLISH) + "><img src='"
+ Functions.getRequestRootPath() + Hudson.RESOURCE_PATH + "/images/none.gif' height=16 width=1>"
+ message + "</div>";
}
};
}
/**
* 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;
}
};
}
/**
* 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 (!Hudson.getInstance().hasPermission(Hudson.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) {
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);
}
Base64.decodeBase64(v);
return ok();
}
/**
* 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) { // XXX 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;
}
//TODO: review and check whether we can do it private
public final Kind kind;
public Kind getKind() {
return 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);
}
}