diff --git a/core/src/main/java/hudson/util/FormFillFailure.java b/core/src/main/java/hudson/util/FormFillFailure.java new file mode 100644 index 0000000000000000000000000000000000000000..d1cec23b42a9ece407d2eee7f5cbe034fbb818f2 --- /dev/null +++ b/core/src/main/java/hudson/util/FormFillFailure.java @@ -0,0 +1,212 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2017, Sun Microsystems, Inc., CloudBees, 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.Functions; +import hudson.Util; +import java.io.IOException; +import java.util.Locale; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +/** + * Represents a failure in a form field doFillXYZ method. + * + *

+ * Use one of the factory methods to create an instance, then throw it from your doFillXyz + * method. + * + * @since FIXME + */ +public abstract class FormFillFailure extends IOException implements HttpResponse { + + /** + * Sends out a string error message that indicates an error. + * + * @param message Human readable message to be sent. + */ + public static FormFillFailure error(@Nonnull String message) { + return errorWithMarkup(Util.escape(message)); + } + + public static FormFillFailure warning(@Nonnull String message) { + return warningWithMarkup(Util.escape(message)); + } + + /** + * Sends out a string error message that indicates an error, + * by formatting it with {@link String#format(String, Object[])} + */ + public static FormFillFailure error(String format, Object... args) { + return error(String.format(format, args)); + } + + public static FormFillFailure warning(String format, Object... args) { + return warning(String.format(format, args)); + } + + /** + * Sends out a string error message, with optional "show details" link that expands to the full stack trace. + * + *

+ * 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 an error needs to be exposed + * to everyone or just those who have higher access to job/hudson/etc. + */ + public static FormFillFailure error(Throwable e, String message) { + return _error(FormValidation.Kind.ERROR, e, message); + } + + public static FormFillFailure warning(Throwable e, String message) { + return _error(FormValidation.Kind.WARNING, e, message); + } + + private static FormFillFailure _error(FormValidation.Kind kind, Throwable e, String message) { + if (e == null) { + return _errorWithMarkup(Util.escape(message), kind); + } + + return _errorWithMarkup(Util.escape(message) + + " " + + Messages.FormValidation_Error_Details() + + "

"
+                + Util.escape(Functions.printThrowable(e)) +
+                "
", kind + ); + } + + public static FormFillFailure error(Throwable e, String format, Object... args) { + return error(e, String.format(format, args)); + } + + public static FormFillFailure warning(Throwable e, String format, Object... args) { + return warning(e, String.format(format, args)); + } + + /** + * Sends out an HTML fragment that indicates an error. + * + *

+ * This method must be used with care to avoid cross-site scripting + * attack. + * + * @param message Human readable message to be sent. error(null) + * can be used as ok(). + */ + public static FormFillFailure errorWithMarkup(String message) { + return _errorWithMarkup(message, FormValidation.Kind.ERROR); + } + + public static FormFillFailure warningWithMarkup(String message) { + return _errorWithMarkup(message, FormValidation.Kind.WARNING); + } + + private static FormFillFailure _errorWithMarkup(@Nonnull final String message, final FormValidation.Kind kind) { + return new FormFillFailure(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 "

" + + message + "
"; + } + + @Override + public String toString() { + return kind + ": " + message; + } + }; + } + + /** + * Sends out an arbitrary HTML fragment as the output. + */ + public static FormFillFailure respond(FormValidation.Kind kind, final String html) { + return new FormFillFailure(kind) { + public String renderHtml() { + return html; + } + + @Override + public String toString() { + return getKind() + ": " + html; + } + }; + } + + private final FormValidation.Kind kind; + private boolean selectionCleared; + + /** + * Instances should be created via one of the factory methods above. + * + * @param kind the kind + */ + private FormFillFailure(FormValidation.Kind kind) { + this.kind = kind; + } + + private FormFillFailure(FormValidation.Kind kind, String message) { + super(message); + this.kind = kind; + } + + public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) + throws IOException, ServletException { + rsp.setContentType("text/html;charset=UTF-8"); + rsp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + rsp.setHeader("X-Jenkins-Select-Error", selectionCleared ? "clear" : "retain"); + rsp.getWriter().print(renderHtml()); + } + + public FormValidation.Kind getKind() { + return kind; + } + + public boolean isSelectionCleared() { + return selectionCleared; + } + + /** + * Flags this failure as requiring that the select options should be cleared out. + * + * @return {@code this} for method chaining. + */ + public FormFillFailure withSelectionCleared() { + this.selectionCleared = true; + return this; + } + + public abstract String renderHtml(); + +} diff --git a/core/src/main/resources/lib/form/select/select.js b/core/src/main/resources/lib/form/select/select.js index 0f87d0bbf66e15b794121272edc70892a0f7cee3..8d0a7a51d6516934e35724e8b35681774d9fcf93 100644 --- a/core/src/main/resources/lib/form/select/select.js +++ b/core/src/main/resources/lib/form/select/select.js @@ -4,8 +4,13 @@ function updateListBox(listBox,url,config) { config = config || {}; config = object(config); var originalOnSuccess = config.onSuccess; - config.onSuccess = function(rsp) { - var l = $(listBox); + var l = $(listBox); + var status = findFollowingTR(listBox, "validation-error-area").firstChild.nextSibling; + if (status.firstChild && status.firstChild.getAttribute('data-select-ajax-error')) { + status.innerHTML = ""; + } + config.onSuccess = function (rsp) { + l.removeClassName("select-ajax-pending"); var currentSelection = l.value; // clear the contents @@ -30,13 +35,24 @@ function updateListBox(listBox,url,config) { if (originalOnSuccess!=undefined) originalOnSuccess(rsp); - }, - config.onFailure = function(rsp) { - // deleting values can result in the data loss, so let's not do that -// var l = $(listBox); -// l.options[0] = null; - } + }; + config.onFailure = function (rsp) { + l.removeClassName("select-ajax-pending"); + status.innerHTML = rsp.responseText; + if (status.firstChild) { + status.firstChild.setAttribute('data-select-ajax-error', 'true') + } + Behaviour.applySubtree(status); + // deleting values can result in the data loss, so let's not do that unless instructed + var header = rsp.getResponseHeader('X-Jenkins-Select-Error'); + if (header && "clear" === header.toLowerCase()) { + // clear the contents + while (l.length > 0) l.options[0] = null; + } + + }; + l.addClassName("select-ajax-pending"); new Ajax.Request(url, config); } @@ -82,4 +98,4 @@ Behaviour.specify("SELECT.select", 'select', 1000, function(e) { } }); }); -}); \ No newline at end of file +}); diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css index 4f9c1fc24bf51a702d2b68bad3f0f9924528c756..75ec6bc749afc1580e7103cfad78fd27b5447d79 100644 --- a/war/src/main/webapp/css/style.css +++ b/war/src/main/webapp/css/style.css @@ -1719,6 +1719,17 @@ table#legend-table td { vertical-align: middle; } +/* ========================= select.jelly ================== */ +select.select-ajax-pending { + padding-left: 1.5em; + padding-top: 0.5em; + padding-bottom: 0.5em; + color: transparent; + background-image: url(../images/spinner.gif); /* this is why here and not in an adjunct */ + background-repeat: no-repeat; + background-position: 2px; +} + /* ========================= Button styles ================= */ .btn-box { display:block;