/hudson-core/src/main/java/hudson/util/FormValidation.java

http://github.com/hudson/hudson · Java · 503 lines · 235 code · 57 blank · 211 comment · 30 complexity · b83c66e632264d38e2698b0e9a55b776 MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2009, Sun Microsystems, Inc.
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson.util;
  25. import hudson.EnvVars;
  26. import hudson.Functions;
  27. import hudson.ProxyConfiguration;
  28. import hudson.Util;
  29. import hudson.FilePath;
  30. import static hudson.Util.fixEmpty;
  31. import hudson.model.Hudson;
  32. import org.kohsuke.stapler.HttpResponse;
  33. import org.kohsuke.stapler.StaplerRequest;
  34. import org.kohsuke.stapler.StaplerResponse;
  35. import org.kohsuke.stapler.Stapler;
  36. import javax.servlet.ServletException;
  37. import java.io.File;
  38. import java.io.IOException;
  39. import java.io.BufferedReader;
  40. import java.io.InputStreamReader;
  41. import java.net.URL;
  42. import java.net.URLConnection;
  43. import java.util.Locale;
  44. /**
  45. * Represents the result of the form field validation.
  46. *
  47. * <p>
  48. * Use one of the factory methods to create an instance, then return it from your <tt>doCheckXyz</tt>
  49. * method. (Via {@link HttpResponse}, the returned object will render the result into {@link StaplerResponse}.)
  50. * This way of designing form field validation allows you to reuse {@code doCheckXyz()} methods
  51. * programmatically as well (by using {@link #kind}.
  52. *
  53. * <p>
  54. * For typical validation needs, this class offers a number of {@code validateXXX(...)} methods, such as
  55. * {@link #validateExecutable(String)}. {@link FilePath} also has a number of {@code validateXXX(...)} methods
  56. * that you may be able to reuse.
  57. *
  58. * <p>
  59. * Also see {@link CVSSCM.DescriptorImpl#doCheckCvsRoot(String)} as an example.
  60. *
  61. * <p>
  62. * This class extends {@link IOException} so that it can be thrown from a method. This allows one to reuse
  63. * the checking logic as a part of the real computation, such as:
  64. *
  65. * <pre>
  66. * String getAntVersion(File antHome) throws FormValidation {
  67. * if(!antHome.isDirectory())
  68. * throw FormValidation.error(antHome+" doesn't look like a home directory");
  69. * ...
  70. * return IOUtils.toString(new File(antHome,"version"));
  71. * }
  72. *
  73. * ...
  74. *
  75. * public FormValidation doCheckAntVersion(@QueryParameter String f) {
  76. * try {
  77. * return ok(getAntVersion(new File(f)));
  78. * } catch (FormValidation f) {
  79. * return f;
  80. * }
  81. * }
  82. *
  83. * ...
  84. *
  85. * public void {@linkplain hudson.tasks.Builder#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.BuildListener) perform}(...) {
  86. * String version = getAntVersion(antHome);
  87. * ...
  88. * }
  89. * </pre>
  90. *
  91. * @author Kohsuke Kawaguchi
  92. * @since 1.294
  93. */
  94. public abstract class FormValidation extends IOException implements HttpResponse {
  95. /**
  96. * Indicates the kind of result.
  97. */
  98. public enum Kind {
  99. /**
  100. * Form field value was OK and no problem was detected.
  101. */
  102. OK,
  103. /**
  104. * Form field value contained something suspicious. For some limited use cases
  105. * the value could be valid, but we suspect the user made a mistake.
  106. */
  107. WARNING,
  108. /**
  109. * Form field value contained a problem that should be corrected.
  110. */
  111. ERROR
  112. }
  113. /**
  114. * Sends out a string error message that indicates an error.
  115. *
  116. * @param message
  117. * Human readable message to be sent. <tt>error(null)</tt>
  118. * can be used as <tt>ok()</tt>.
  119. */
  120. public static FormValidation error(String message) {
  121. return errorWithMarkup(message==null?null: Util.escape(message));
  122. }
  123. public static FormValidation warning(String message) {
  124. return warningWithMarkup(message==null?null:Util.escape(message));
  125. }
  126. public static FormValidation ok(String message) {
  127. return okWithMarkup(message==null?null:Util.escape(message));
  128. }
  129. /**
  130. * Singleton instance that represents "OK".
  131. */
  132. private static final FormValidation OK = respond(Kind.OK,"<div/>");
  133. public static FormValidation ok() {
  134. return OK;
  135. }
  136. /**
  137. * Sends out a string error message that indicates an error,
  138. * by formatting it with {@link String#format(String, Object[])}
  139. */
  140. public static FormValidation error(String format, Object... args) {
  141. return error(String.format(format,args));
  142. }
  143. public static FormValidation warning(String format, Object... args) {
  144. return warning(String.format(format,args));
  145. }
  146. public static FormValidation ok(String format, Object... args) {
  147. return ok(String.format(format,args));
  148. }
  149. /**
  150. * Sends out a string error message, with optional "show details" link that expands to the full stack trace.
  151. *
  152. * <p>
  153. * Use this with caution, so that anonymous users do not gain too much insights into the state of the system,
  154. * as error stack trace often reveals a lot of information. Consider if a check operation needs to be exposed
  155. * to everyone or just those who have higher access to job/hudson/etc.
  156. */
  157. public static FormValidation error(Throwable e, String message) {
  158. return _error(Kind.ERROR, e, message);
  159. }
  160. public static FormValidation warning(Throwable e, String message) {
  161. return _error(Kind.WARNING, e, message);
  162. }
  163. private static FormValidation _error(Kind kind, Throwable e, String message) {
  164. if (e==null) return _errorWithMarkup(Util.escape(message),kind);
  165. return _errorWithMarkup(Util.escape(message)+
  166. " <a href='#' class='showDetails'>"
  167. + Messages.FormValidation_Error_Details()
  168. + "</a><pre style='display:none'>"
  169. + Functions.printThrowable(e) +
  170. "</pre>",kind
  171. );
  172. }
  173. public static FormValidation error(Throwable e, String format, Object... args) {
  174. return error(e,String.format(format,args));
  175. }
  176. public static FormValidation warning(Throwable e, String format, Object... args) {
  177. return warning(e,String.format(format,args));
  178. }
  179. /**
  180. * Sends out an HTML fragment that indicates an error.
  181. *
  182. * <p>
  183. * This method must be used with care to avoid cross-site scripting
  184. * attack.
  185. *
  186. * @param message
  187. * Human readable message to be sent. <tt>error(null)</tt>
  188. * can be used as <tt>ok()</tt>.
  189. */
  190. public static FormValidation errorWithMarkup(String message) {
  191. return _errorWithMarkup(message,Kind.ERROR);
  192. }
  193. public static FormValidation warningWithMarkup(String message) {
  194. return _errorWithMarkup(message,Kind.WARNING);
  195. }
  196. public static FormValidation okWithMarkup(String message) {
  197. return _errorWithMarkup(message,Kind.OK);
  198. }
  199. private static FormValidation _errorWithMarkup(final String message, final Kind kind) {
  200. if(message==null)
  201. return ok();
  202. return new FormValidation(kind, message) {
  203. public String renderHtml() {
  204. // 1x16 spacer needed for IE since it doesn't support min-height
  205. return "<div class="+ kind.name().toLowerCase(Locale.ENGLISH) +"><img src='"+
  206. Stapler.getCurrentRequest().getContextPath()+ Hudson.RESOURCE_PATH+"/images/none.gif' height=16 width=1>"+
  207. message+"</div>";
  208. }
  209. };
  210. }
  211. /**
  212. * Sends out an arbitrary HTML fragment as the output.
  213. */
  214. public static FormValidation respond(Kind kind, final String html) {
  215. return new FormValidation(kind) {
  216. public String renderHtml() {
  217. return html;
  218. }
  219. };
  220. }
  221. /**
  222. * Performs an application-specific validation on the given file.
  223. *
  224. * <p>
  225. * This is used as a piece in a bigger validation effort.
  226. */
  227. public static abstract class FileValidator {
  228. public abstract FormValidation validate(File f);
  229. /**
  230. * Singleton instance that does no check.
  231. */
  232. public static final FileValidator NOOP = new FileValidator() {
  233. public FormValidation validate(File f) {
  234. return ok();
  235. }
  236. };
  237. }
  238. /**
  239. * Makes sure that the given string points to an executable file.
  240. */
  241. public static FormValidation validateExecutable(String exe) {
  242. return validateExecutable(exe, FileValidator.NOOP);
  243. }
  244. /**
  245. * Makes sure that the given string points to an executable file.
  246. *
  247. * @param exeValidator
  248. * If the validation process discovers a valid executable program on the given path,
  249. * the specified {@link FileValidator} can perform additional checks (such as making sure
  250. * that it has the right version, etc.)
  251. */
  252. public static FormValidation validateExecutable(String exe, FileValidator exeValidator) {
  253. // insufficient permission to perform validation?
  254. if(!Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) return ok();
  255. exe = fixEmpty(exe);
  256. if(exe==null)
  257. return ok();
  258. if(exe.indexOf(File.separatorChar)>=0) {
  259. // this is full path
  260. File f = new File(exe);
  261. if(f.exists()) return exeValidator.validate(f);
  262. File fexe = new File(exe+".exe");
  263. if(fexe.exists()) return exeValidator.validate(fexe);
  264. return error("There's no such file: "+exe);
  265. }
  266. // look in PATH
  267. String path = EnvVars.masterEnvVars.get("PATH");
  268. String tokenizedPath = "";
  269. String delimiter = null;
  270. if(path!=null) {
  271. for (String _dir : Util.tokenize(path.replace("\\", "\\\\"),File.pathSeparator)) {
  272. if (delimiter == null) {
  273. delimiter = ", ";
  274. }
  275. else {
  276. tokenizedPath += delimiter;
  277. }
  278. tokenizedPath += _dir.replace('\\', '/');
  279. File dir = new File(_dir);
  280. File f = new File(dir,exe);
  281. if(f.exists()) return exeValidator.validate(f);
  282. File fexe = new File(dir,exe+".exe");
  283. if(fexe.exists()) return exeValidator.validate(fexe);
  284. }
  285. tokenizedPath += ".";
  286. } else {
  287. tokenizedPath = "unavailable.";
  288. }
  289. // didn't find it
  290. return error("There's no such executable "+exe+" in PATH: "+tokenizedPath);
  291. }
  292. /**
  293. * Makes sure that the given string is a non-negative integer.
  294. */
  295. public static FormValidation validateNonNegativeInteger(String value) {
  296. try {
  297. if(Integer.parseInt(value)<0)
  298. return error(hudson.model.Messages.Hudson_NotANonNegativeNumber());
  299. return ok();
  300. } catch (NumberFormatException e) {
  301. return error(hudson.model.Messages.Hudson_NotANumber());
  302. }
  303. }
  304. /**
  305. * Makes sure that the given string is a positive integer.
  306. */
  307. public static FormValidation validatePositiveInteger(String value) {
  308. try {
  309. if(Integer.parseInt(value)<=0)
  310. return error(hudson.model.Messages.Hudson_NotAPositiveNumber());
  311. return ok();
  312. } catch (NumberFormatException e) {
  313. return error(hudson.model.Messages.Hudson_NotANumber());
  314. }
  315. }
  316. /**
  317. * Makes sure that the given string is not null or empty.
  318. */
  319. public static FormValidation validateRequired(String value) {
  320. if (Util.fixEmptyAndTrim(value) == null)
  321. return error(Messages.FormValidation_ValidateRequired());
  322. return ok();
  323. }
  324. /**
  325. * Makes sure that the given string is a base64 encoded text.
  326. *
  327. * @param allowWhitespace
  328. * if you allow whitespace (CR,LF,etc) in base64 encoding
  329. * @param allowEmpty
  330. * Is empty string allowed?
  331. * @param errorMessage
  332. * Error message.
  333. * @since 1.305
  334. */
  335. public static FormValidation validateBase64(String value, boolean allowWhitespace, boolean allowEmpty, String errorMessage) {
  336. try {
  337. String v = value;
  338. if(!allowWhitespace) {
  339. if(v.indexOf(' ')>=0 || v.indexOf('\n')>=0)
  340. return error(errorMessage);
  341. }
  342. v=v.trim();
  343. if(!allowEmpty && v.length()==0)
  344. return error(errorMessage);
  345. com.trilead.ssh2.crypto.Base64.decode(v.toCharArray());
  346. return ok();
  347. } catch (IOException e) {
  348. return error(errorMessage);
  349. }
  350. }
  351. /**
  352. * Convenient base class for checking the validity of URLs.
  353. *
  354. * <p>
  355. * This allows the check method to call various utility methods in a concise syntax.
  356. */
  357. public static abstract class URLCheck {
  358. /**
  359. * Opens the given URL and reads text content from it.
  360. * This method honors Content-type header.
  361. */
  362. protected BufferedReader open(URL url) throws IOException {
  363. // use HTTP content type to find out the charset.
  364. URLConnection con = ProxyConfiguration.open(url);
  365. if (con == null) { // XXX is this even permitted by URL.openConnection?
  366. throw new IOException(url.toExternalForm());
  367. }
  368. return new BufferedReader(
  369. new InputStreamReader(con.getInputStream(),getCharset(con)));
  370. }
  371. /**
  372. * Finds the string literal from the given reader.
  373. * @return
  374. * true if found, false otherwise.
  375. */
  376. protected boolean findText(BufferedReader in, String literal) throws IOException {
  377. String line;
  378. while((line=in.readLine())!=null)
  379. if(line.indexOf(literal)!=-1)
  380. return true;
  381. return false;
  382. }
  383. /**
  384. * Calls the {@link FormValidation#error(String)} method with a reasonable error message.
  385. * Use this method when the {@link #open(URL)} or {@link #findText(BufferedReader, String)} fails.
  386. *
  387. * @param url
  388. * Pass in the URL that was connected. Used for error diagnosis.
  389. */
  390. protected FormValidation handleIOException(String url, IOException e) throws IOException, ServletException {
  391. // any invalid URL comes here
  392. if(e.getMessage().equals(url))
  393. // Sun JRE (and probably others too) often return just the URL in the error.
  394. return error("Unable to connect "+url);
  395. else
  396. return error(e.getMessage());
  397. }
  398. /**
  399. * Figures out the charset from the content-type header.
  400. */
  401. private String getCharset(URLConnection con) {
  402. for( String t : con.getContentType().split(";") ) {
  403. t = t.trim().toLowerCase(Locale.ENGLISH);
  404. if(t.startsWith("charset="))
  405. return t.substring(8);
  406. }
  407. // couldn't find it. HTML spec says default is US-ASCII,
  408. // but UTF-8 is a better choice since
  409. // (1) it's compatible with US-ASCII
  410. // (2) a well-written web applications tend to use UTF-8
  411. return "UTF-8";
  412. }
  413. /**
  414. * Implement the actual form validation logic, by using other convenience methosd defined in this class.
  415. * If you are not using any of those, you don't need to extend from this class.
  416. */
  417. protected abstract FormValidation check() throws IOException, ServletException;
  418. }
  419. //TODO: review and check whether we can do it private
  420. public final Kind kind;
  421. public Kind getKind() {
  422. return kind;
  423. }
  424. /**
  425. * Instances should be created via one of the factory methods above.
  426. * @param kind
  427. */
  428. private FormValidation(Kind kind) {
  429. this.kind = kind;
  430. }
  431. private FormValidation(Kind kind, String message) {
  432. super(message);
  433. this.kind = kind;
  434. }
  435. public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
  436. respond(rsp, renderHtml());
  437. }
  438. public abstract String renderHtml();
  439. /**
  440. * Sends out an arbitrary HTML fragment as the output.
  441. */
  442. protected void respond(StaplerResponse rsp, String html) throws IOException, ServletException {
  443. rsp.setContentType("text/html;charset=UTF-8");
  444. rsp.getWriter().print(html);
  445. }
  446. }