/******************************************************************************* * * 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: * * Kohsuke Kawaguchi, Jene Jasper, Tom Huybrechts * * *******************************************************************************/ package hudson.util; import static hudson.Util.fixEmpty; import hudson.EnvVars; import hudson.FilePath; import hudson.Functions; import hudson.ProxyConfiguration; import hudson.Util; import hudson.model.AbstractProject; import hudson.model.Hudson; import hudson.model.Item; import hudson.security.Permission; import hudson.security.AccessControlled; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.util.Locale; import javax.servlet.ServletException; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.Stapler; import org.springframework.security.access.AccessDeniedException; /** * Base class that provides the framework for doing on-the-fly form field * validation. * * <p> The {@link #check()} method is to be implemented by derived classes to * perform the validation. See hudson-behavior.js 'validated' CSS class and * 'checkUrl' attribute. * * @author Kohsuke Kawaguchi * @deprecated as of 1.294 Use {@link FormValidation} as a return value in your * check method. */ public abstract class FormFieldValidator { public static final Permission CHECK = Hudson.ADMINISTER; protected final StaplerRequest request; protected final StaplerResponse response; /** * Permission to check, or null if this check doesn't require any * permission. */ protected final Permission permission; /** * The object to which the permission is checked against. If * {@link #permission} is non-null, must be non-null. */ protected final AccessControlled subject; /** * @param adminOnly Pass true to only let admin users to run the check. This * is necessary for security reason, so that unauthenticated user cannot * obtain sensitive information or run a process that may have side-effect. */ protected FormFieldValidator(StaplerRequest request, StaplerResponse response, boolean adminOnly) { this(request, response, adminOnly ? Hudson.getInstance() : null, adminOnly ? CHECK : null); } /** * @deprecated Use {@link #FormFieldValidator(Permission)} and remove * {@link StaplerRequest} and {@link StaplerResponse} from your "doCheck..." * method parameter */ protected FormFieldValidator(StaplerRequest request, StaplerResponse response, Permission permission) { this(request, response, Hudson.getInstance(), permission); } /** * @param permission Permission needed to perform this validation, or null * if no permission is necessary. */ protected FormFieldValidator(Permission permission) { this(Stapler.getCurrentRequest(), Stapler.getCurrentResponse(), permission); } /** * @deprecated Use {@link #FormFieldValidator(AccessControlled,Permission)} * and remove {@link StaplerRequest} and {@link StaplerResponse} from your * "doCheck..." method parameter */ protected FormFieldValidator(StaplerRequest request, StaplerResponse response, AccessControlled subject, Permission permission) { this.request = request; this.response = response; this.subject = subject; this.permission = permission; } protected FormFieldValidator(AccessControlled subject, Permission permission) { this(Stapler.getCurrentRequest(), Stapler.getCurrentResponse(), subject, permission); } /** * Runs the validation code. */ public final void process() throws IOException, ServletException { if (permission != null) { try { if (subject == null) { throw new AccessDeniedException("No subject"); } subject.checkPermission(permission); } catch (AccessDeniedException e) { // if the user has hudson-wisde admin permission, all checks are allowed // this is to protect Hudson administrator from broken ACL/SecurityRealm implementation/configuration. if (!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) { throw e; } } } check(); } protected abstract void check() throws IOException, ServletException; /** * Gets the parameter as a file. */ protected final File getFileParameter(String paramName) { return new File(Util.fixNull(request.getParameter(paramName))); } /** * Sends out an HTML fragment that indicates a success. */ public void ok() throws IOException, ServletException { respond("<div/>"); } /** * Sends out an arbitrary HTML fragment. */ public void respond(String html) throws IOException, ServletException { response.setContentType("text/html"); response.getWriter().print(html); } /** * 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 void error(String message) throws IOException, ServletException { errorWithMarkup(message == null ? null : Util.escape(message)); } public void warning(String message) throws IOException, ServletException { warningWithMarkup(message == null ? null : Util.escape(message)); } public void ok(String message) throws IOException, ServletException { okWithMarkup(message == null ? null : Util.escape(message)); } /** * Sends out a string error message that indicates an error, by formatting * it with {@link String#format(String, Object[])} */ public void error(String format, Object... args) throws IOException, ServletException { error(String.format(format, args)); } public void warning(String format, Object... args) throws IOException, ServletException { warning(String.format(format, args)); } public void ok(String format, Object... args) throws IOException, ServletException { ok(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 void errorWithMarkup(String message) throws IOException, ServletException { _errorWithMarkup(message, "error"); } public void warningWithMarkup(String message) throws IOException, ServletException { _errorWithMarkup(message, "warning"); } public void okWithMarkup(String message) throws IOException, ServletException { _errorWithMarkup(message, "ok"); } private void _errorWithMarkup(String message, String cssClass) throws IOException, ServletException { if (message == null) { ok(); } else { response.setContentType("text/html;charset=UTF-8"); // 1x16 spacer needed for IE since it doesn't support min-height response.getWriter().print("<div class=" + cssClass + "><img src='" + Functions.getRequestRootPath(request) + Hudson.RESOURCE_PATH + "/images/none.gif' height=16 width=1>" + message + "</div>"); } } /** * Convenient base class for checking the validity of URLs * * @deprecated as of 1.294 Use {@link FormValidation.URLCheck} */ public static abstract class URLCheck extends FormFieldValidator { public URLCheck(StaplerRequest request, StaplerResponse response) { // can be used to check the existence of any file in file system // or other HTTP URLs inside firewall, so limit this to the admin. super(request, response, true); } /** * 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 #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 void 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. { error("Unable to connect " + url); } else { 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"; } } /** * Checks if the given value is an URL to some Hudson's top page. * * @since 1.192 */ public static class HudsonURL extends URLCheck { public HudsonURL(StaplerRequest request, StaplerResponse response) { super(request, response); } protected void check() throws IOException, ServletException { String value = fixEmpty(request.getParameter("value")); if (value == null) { // nothing entered yet ok(); return; } if (!value.endsWith("/")) { value += '/'; } try { URL url = new URL(value); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.connect(); if (con.getResponseCode() != 200 || con.getHeaderField("X-Hudson") == null) { error(value + " is not Hudson (" + con.getResponseMessage() + ")"); return; } ok(); } catch (IOException e) { handleIOException(value, e); } } } /** * Checks the file mask (specified in the 'value' query parameter) against * the current workspace. * * @since 1.90. * @deprecated as of 1.294. Use * {@link FilePath#validateFileMask(String, boolean)} */ public static class WorkspaceFileMask extends FormFieldValidator { private final boolean errorIfNotExist; public WorkspaceFileMask(StaplerRequest request, StaplerResponse response) { this(request, response, true); } public WorkspaceFileMask(StaplerRequest request, StaplerResponse response, boolean errorIfNotExist) { // Require CONFIGURE permission on the job super(request, response, request.findAncestorObject(AbstractProject.class), Item.CONFIGURE); this.errorIfNotExist = errorIfNotExist; } protected void check() throws IOException, ServletException { String value = fixEmpty(request.getParameter("value")); AbstractProject<?, ?> p = (AbstractProject<?, ?>) subject; if (value == null || p == null) { ok(); // none entered yet, or something is seriously wrong return; } try { FilePath ws = getBaseDirectory(p); if (ws == null || !ws.exists()) { // no workspace. can't check ok(); return; } String msg = ws.validateAntFileMask(value); if (errorIfNotExist) { error(msg); } else { warning(msg); } } catch (InterruptedException e) { ok(); // coundn't check } } /** * The base directory from which the path name is resolved. */ protected FilePath getBaseDirectory(AbstractProject<?, ?> p) { return p.getSomeWorkspace(); } } /** * Checks a valid directory name (specified in the 'value' query parameter) * against the current workspace. * * @since 1.116 * @deprecated as of 1.294. Use * {@link FilePath#validateRelativeDirectory(String, boolean)} (see * {@link hudson.tasks.JavadocArchiver.DescriptorImpl#doCheck(AbstractProject, String)} */ public static class WorkspaceDirectory extends WorkspaceFilePath { public WorkspaceDirectory(StaplerRequest request, StaplerResponse response, boolean errorIfNotExist) { super(request, response, errorIfNotExist, false); } public WorkspaceDirectory(StaplerRequest request, StaplerResponse response) { this(request, response, true); } } /** * Checks a valid file name or directory (specified in the 'value' query * parameter) against the current workspace. * * @since 1.160 * @deprecated as of 1.294. Use * {@link FilePath#validateRelativePath(String, boolean, boolean)} */ public static class WorkspaceFilePath extends FormFieldValidator { private final boolean errorIfNotExist; private final boolean expectingFile; public WorkspaceFilePath(StaplerRequest request, StaplerResponse response, boolean errorIfNotExist, boolean expectingFile) { // Require CONFIGURE permission on this job super(request, response, request.findAncestorObject(AbstractProject.class), Item.CONFIGURE); this.errorIfNotExist = errorIfNotExist; this.expectingFile = expectingFile; } protected void check() throws IOException, ServletException { String value = fixEmpty(request.getParameter("value")); AbstractProject<?, ?> p = (AbstractProject<?, ?>) subject; if (value == null || p == null) { ok(); // none entered yet, or something is seriously wrong return; } if (value.contains("*")) { // a common mistake is to use wildcard error("Wildcard is not allowed here"); return; } try { FilePath ws = getBaseDirectory(p); if (ws == null) { // can't check ok(); return; } if (!ws.exists()) { // no workspace. can't check ok(); return; } if (ws.child(value).exists()) { if (expectingFile) { if (!ws.child(value).isDirectory()) { ok(); } else { error(value + " is not a file"); } } else { if (ws.child(value).isDirectory()) { ok(); } else { error(value + " is not a directory"); } } } else { String msg = "No such " + (expectingFile ? "file" : "directory") + ": " + value; if (errorIfNotExist) { error(msg); } else { warning(msg); } } } catch (InterruptedException e) { ok(); // coundn't check } } /** * The base directory from which the path name is resolved. */ protected FilePath getBaseDirectory(AbstractProject<?, ?> p) { return p.getSomeWorkspace(); } } /** * Checks a valid executable binary (specified in the 'value' query * parameter). It has to be either given as a full path to the executable, * or else it will be searched in PATH. * * <p> This file also handles ".exe" omission in Windows --- I thought * Windows has actually more generic mechanism for the executable extension * omission, so perhaps this needs to be extended to handle that correctly. * More info needed. * * @since 1.124 * @deprecated as of 1.294. Use * {@link FormValidation#validateExecutable(String)} */ public static class Executable extends FormFieldValidator { public Executable(StaplerRequest request, StaplerResponse response) { // Require admin permission super(request, response, true); } protected void check() throws IOException, ServletException { String exe = fixEmpty(request.getParameter("value")); if (exe == null) { ok(); // nothing entered yet return; } if (exe.indexOf(File.separatorChar) >= 0) { // this is full path File f = new File(exe); if (f.exists()) { checkExecutable(f); return; } File fexe = new File(exe + ".exe"); if (fexe.exists()) { checkExecutable(fexe); return; } error("There's no such file: " + exe); } else { // 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()) { checkExecutable(f); return; } File fexe = new File(dir, exe + ".exe"); if (fexe.exists()) { checkExecutable(fexe); return; } } tokenizedPath += "."; } else { tokenizedPath = "unavailable."; } // didn't find it error("There's no such executable " + exe + " in PATH: " + tokenizedPath); } } /** * Provides an opportunity for derived classes to do additional checks * on the executable. */ protected void checkExecutable(File exe) throws IOException, ServletException { ok(); } } /** * Verifies that the 'value' parameter is correct base64 encoded text. * * @since 1.257 * @deprecated as of 1.305 Use * {@link FormValidation#validateBase64(String, boolean, boolean, String)} * instead. */ public static class Base64 extends FormFieldValidator { private final boolean allowWhitespace; private final boolean allowEmpty; private final String errorMessage; public Base64(StaplerRequest request, StaplerResponse response, boolean allowWhitespace, boolean allowEmpty, String errorMessage) { super(request, response, false); this.allowWhitespace = allowWhitespace; this.allowEmpty = allowEmpty; this.errorMessage = errorMessage; } protected void check() throws IOException, ServletException { try { String v = request.getParameter("value"); if (!allowWhitespace) { if (v.indexOf(' ') >= 0 || v.indexOf('\n') >= 0) { fail(); return; } } v = v.trim(); if (!allowEmpty && v.length() == 0) { fail(); return; } org.apache.commons.codec.binary.Base64.decodeBase64(v); ok(); } catch (IOException e) { fail(); } } protected void fail() throws IOException, ServletException { error(errorMessage); } } /** * Verifies that the 'value' parameter is an integer >= 0. * * @since 1.282 * @deprecated as of 1.294 Use * {@link FormValidation#validateNonNegativeInteger(String)} */ public static class NonNegativeInteger extends FormFieldValidator { public NonNegativeInteger() { super(null); } protected void check() throws IOException, ServletException { try { String value = request.getParameter("value"); if (Integer.parseInt(value) < 0) { error(hudson.model.Messages.Hudson_NotAPositiveNumber()); } else { ok(); } } catch (NumberFormatException e) { error(hudson.model.Messages.Hudson_NotANumber()); } } } }