/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Yahoo! Inc., Stephen Connolly, Tom Huybrechts, Alan Harder, Manufacture
* Francaise des Pneumatiques Michelin, Romain Seguy
*
* 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;
import hudson.model.Slave;
import hudson.security.*;
import jenkins.telemetry.impl.AutoRefresh;
import jenkins.util.SystemProperties;
import hudson.cli.CLICommand;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotatorFactory;
import hudson.init.InitMilestone;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.DescriptorVisibilityFilter;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.JDK;
import hudson.model.Job;
import hudson.model.JobPropertyDescriptor;
import hudson.model.ModelObject;
import hudson.model.Node;
import hudson.model.PageDecorator;
import jenkins.model.SimplePageDecorator;
import hudson.model.PaneStatusProperties;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterDefinition.ParameterDescriptor;
import hudson.model.Run;
import hudson.model.TimeZoneProperty;
import hudson.model.TopLevelItem;
import hudson.model.User;
import hudson.model.View;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.search.SearchableModelObject;
import hudson.security.captcha.CaptchaSupport;
import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.Cloud;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
import hudson.slaves.RetentionStrategy;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.tasks.UserAvatarResolver;
import hudson.util.Area;
import hudson.util.FormValidation.CheckMethod;
import hudson.util.HudsonIsLoading;
import hudson.util.HudsonIsRestarting;
import hudson.util.Iterators;
import hudson.util.jna.GNUCLibrary;
import hudson.util.Secret;
import hudson.views.MyViewsTabBar;
import hudson.views.ViewsTabBar;
import hudson.widgets.RenderOnDemandClosure;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.ModelObjectWithContextMenu;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jexl.parser.ASTSizeFunction;
import org.apache.commons.jexl.util.Introspector;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.icon.IconSet;
import org.jvnet.tiger_types.Types;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.jelly.InternationalizedStringExpression.RawHtmlArgument;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import hudson.model.PasswordParameterDefinition;
import hudson.util.RunList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.apache.commons.io.IOUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.accmod.restrictions.DoNotUse;
/**
* Utility functions used in views.
*
*
* An instance of this class is created for each request and made accessible
* from view pages via the variable 'h' (h stands for Hudson.)
*
* @author Kohsuke Kawaguchi
*/
@SuppressWarnings("rawtypes")
public class Functions {
private static final AtomicLong iota = new AtomicLong();
private static Logger LOGGER = Logger.getLogger(Functions.class.getName());
public Functions() {
}
/**
* Generates an unique ID.
*/
public String generateId() {
return "id" + iota.getAndIncrement();
}
public static boolean isModel(Object o) {
return o instanceof ModelObject;
}
public static boolean isModelWithContextMenu(Object o) {
return o instanceof ModelObjectWithContextMenu;
}
public static boolean isModelWithChildren(Object o) {
return o instanceof ModelObjectWithChildren;
}
@Deprecated
public static boolean isMatrixProject(Object o) {
return o != null && o.getClass().getName().equals("hudson.matrix.MatrixProject");
}
public static String xsDate(Calendar cal) {
return Util.XS_DATETIME_FORMATTER.format(cal.getTime());
}
public static String rfc822Date(Calendar cal) {
return Util.RFC822_DATETIME_FORMATTER.format(cal.getTime());
}
/**
* During Jenkins start-up, before {@link InitMilestone#PLUGINS_STARTED} the extensions lists will be empty
* and they are not guaranteed to be fully populated until after {@link InitMilestone#EXTENSIONS_AUGMENTED},
* similarly, during termination after {@link Jenkins#isTerminating()} is set, it is no longer safe to access
* the extensions lists.
* If you attempt to access the extensions list from a UI thread while the extensions are being loaded you will
* hit a big honking great monitor lock that will block until the effective extension list has been determined
* (as if a plugin fails to start, all of the failed plugin's extensions and any dependent plugins' extensions
* will have to be evicted from the list of extensions. In practical terms this only affects the
* "Jenkins is loading" screen, but as that screen uses the generic layouts we provide this utility method
* so that the generic layouts can avoid iterating extension lists while Jenkins is starting up.
* If you attempt to access the extensions list from a UI thread while Jenkins is being shut down, the extensions
* themselves may no longer be in a valid state and could attempt to revive themselves and block termination.
* In actual terms the termination only affects those views required to render {@link HudsonIsRestarting}'s
* {@code index.jelly} which is the same set as the {@link HudsonIsLoading} pages so it makes sense to
* use both checks here.
*
* @return {@code true} if the extensions lists have been populated.
* @since 1.607
*/
public static boolean isExtensionsAvailable() {
final Jenkins jenkins = Jenkins.getInstanceOrNull();
return jenkins != null && jenkins.getInitLevel().compareTo(InitMilestone.EXTENSIONS_AUGMENTED) >= 0
&& !jenkins.isTerminating();
}
public static void initPageVariables(JellyContext context) {
StaplerRequest currentRequest = Stapler.getCurrentRequest();
currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse());
String rootURL = currentRequest.getContextPath();
Functions h = new Functions();
context.setVariable("h", h);
// The path starts with a "/" character but does not end with a "/" character.
context.setVariable("rootURL", rootURL);
/*
load static resources from the path dedicated to a specific version.
This "/static/VERSION/abc/def.ghi" path is interpreted by stapler to be
the same thing as "/abc/def.ghi", but this avoids the stale cache
problem when the user upgrades to new Jenkins. Stapler also sets a long
future expiration dates for such static resources.
see https://wiki.jenkins-ci.org/display/JENKINS/Hyperlinks+in+HTML
*/
context.setVariable("resURL",rootURL+getResourcePath());
context.setVariable("imagesURL",rootURL+getResourcePath()+"/images");
context.setVariable("userAgent", currentRequest.getHeader("User-Agent"));
IconSet.initPageVariables(context);
}
/**
* Given {@code c=MyList (extends ArrayList), base=List}, compute the parameterization of 'base'
* that's assignable from 'c' (in this case {@code List}), and return its n-th type parameter
* (n=0 would return {@code Foo}).
*
*
* This method is useful for doing type arithmetic.
*
* @throws AssertionError
* if c' is not parameterized.
*/
public static Class getTypeParameter(Class extends B> c, Class base, int n) {
Type parameterization = Types.getBaseClass(c,base);
if (parameterization instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) parameterization;
return Types.erasure(Types.getTypeArgument(pt,n));
} else {
throw new AssertionError(c+" doesn't properly parameterize "+base);
}
}
public JDK.DescriptorImpl getJDKDescriptor() {
return Jenkins.get().getDescriptorByType(JDK.DescriptorImpl.class);
}
/**
* Prints the integer as a string that represents difference,
* like "-5", "+/-0", "+3".
*/
public static String getDiffString(int i) {
if(i==0) return "±0";
String s = Integer.toString(i);
if(i>0) return "+"+s;
else return s;
}
/**
* {@link #getDiffString(int)} that doesn't show anything for +/-0
*/
public static String getDiffString2(int i) {
if(i==0) return "";
String s = Integer.toString(i);
if(i>0) return "+"+s;
else return s;
}
/**
* {@link #getDiffString2(int)} that puts the result into prefix and suffix
* if there's something to print
*/
public static String getDiffString2(String prefix, int i, String suffix) {
if(i==0) return "";
String s = Integer.toString(i);
if(i>0) return prefix+"+"+s+suffix;
else return prefix+s+suffix;
}
/**
* Adds the proper suffix.
*/
public static String addSuffix(int n, String singular, String plural) {
StringBuilder buf = new StringBuilder();
buf.append(n).append(' ');
if(n==1)
buf.append(singular);
else
buf.append(plural);
return buf.toString();
}
public static RunUrl decompose(StaplerRequest req) {
List ancestors = req.getAncestors();
// find the first and last Run instances
Ancestor f=null,l=null;
for (Ancestor anc : ancestors) {
if(anc.getObject() instanceof Run) {
if(f==null) f=anc;
l=anc;
}
}
if(l==null) return null; // there was no Run object
String head = f.getPrev().getUrl()+'/';
String base = l.getUrl();
String reqUri = req.getOriginalRequestURI();
// Find "rest" or URI by removing N path components.
// Not using reqUri.substring(f.getUrl().length()) to avoid mismatches due to
// url-encoding or extra slashes. Former may occur in Tomcat (despite the spec saying
// this string is not decoded, Tomcat apparently decodes this string. You see ' '
// instead of '%20', which is what the browser has sent), latter may occur in some
// proxy or URL-rewriting setups where extra slashes are inadvertently added.
String furl = f.getUrl();
int slashCount = 0;
// Count components in ancestor URL
for (int i = furl.indexOf('/'); i >= 0; i = furl.indexOf('/', i + 1)) slashCount++;
// Remove that many from request URL, ignoring extra slashes
String rest = reqUri.replaceFirst("(?:/+[^/]*){" + slashCount + "}", "");
return new RunUrl( (Run) f.getObject(), head, base, rest);
}
/**
* If we know the user's screen resolution, return it. Otherwise null.
* @since 1.213
*/
public static Area getScreenResolution() {
Cookie res = Functions.getCookie(Stapler.getCurrentRequest(),"screenResolution");
if(res!=null)
return Area.parse(res.getValue());
return null;
}
@Restricted(NoExternalUse.class)
public static boolean useHidingPasswordFields() {
return SystemProperties.getBoolean(Functions.class.getName() + ".hidingPasswordFields", true);
}
/**
* URL decomposed for easier computation of relevant URLs.
*
*
* The decomposed URL will be of the form:
*
* aaaaaa/524/bbbbb/cccc
* -head-| N |---rest---
* ----- base -----|
*
*
*
* The head portion is the part of the URL from the {@link jenkins.model.Jenkins}
* object to the first {@link Run} subtype. When "next/prev build"
* is chosen, this part remains intact.
*
*
* The {@code 524} is the path from {@link Job} to {@link Run}.
*
*
* The {@code bbb} portion is the path after that till the last
* {@link Run} subtype. The {@code ccc} portion is the part
* after that.
*/
public static final class RunUrl {
private final String head, base, rest;
private final Run run;
public RunUrl(Run run, String head, String base, String rest) {
this.run = run;
this.head = head;
this.base = base;
this.rest = rest;
}
public String getBaseUrl() {
return base;
}
/**
* Returns the same page in the next build.
*/
public String getNextBuildUrl() {
return getUrl(run.getNextBuild());
}
/**
* Returns the same page in the previous build.
*/
public String getPreviousBuildUrl() {
return getUrl(run.getPreviousBuild());
}
private String getUrl(Run n) {
if(n ==null)
return null;
else {
return head+n.getNumber()+rest;
}
}
}
public static Node.Mode[] getNodeModes() {
return Node.Mode.values();
}
public static String getProjectListString(List projects) {
return Items.toNameList(projects);
}
/**
* @deprecated as of 1.294
* JEXL now supports the real ternary operator "x?y:z", so this work around
* is no longer necessary.
*/
@Deprecated
public static Object ifThenElse(boolean cond, Object thenValue, Object elseValue) {
return cond ? thenValue : elseValue;
}
public static String appendIfNotNull(String text, String suffix, String nullText) {
return text == null ? nullText : text + suffix;
}
public static Map getSystemProperties() {
return new TreeMap<>(System.getProperties());
}
/**
* Gets the system property indicated by the specified key.
*
* Delegates to {@link SystemProperties#getString(java.lang.String)}.
*/
@Restricted(DoNotUse.class)
public static String getSystemProperty(String key) {
return SystemProperties.getString(key);
}
public static Map getEnvVars() {
return new TreeMap<>(EnvVars.masterEnvVars);
}
public static boolean isWindows() {
return File.pathSeparatorChar==';';
}
public static boolean isGlibcSupported() {
try {
GNUCLibrary.LIBC.getpid();
return true;
} catch(Throwable t) {
return false;
}
}
public static List getLogRecords() {
return Jenkins.logRecords;
}
public static String printLogRecord(LogRecord r) {
return formatter.format(r);
}
@Restricted(NoExternalUse.class)
public static String[] printLogRecordHtml(LogRecord r, LogRecord prior) {
String[] oldParts = prior == null ? new String[4] : logRecordPreformat(prior);
String[] newParts = logRecordPreformat(r);
for (int i = 0; i < /* not 4 */3; i++) {
newParts[i] = "" + newParts[i] + "";
}
newParts[3] = Util.xmlEscape(newParts[3]);
return newParts;
}
/**
* Partially formats a log record.
* @return date, source, level, message+thrown
* @see SimpleFormatter#format(LogRecord)
*/
private static String[] logRecordPreformat(LogRecord r) {
String source;
if (r.getSourceClassName() == null) {
source = r.getLoggerName();
} else {
if (r.getSourceMethodName() == null) {
source = r.getSourceClassName();
} else {
source = r.getSourceClassName() + " " + r.getSourceMethodName();
}
}
String message = new SimpleFormatter().formatMessage(r) + "\n";
Throwable x = r.getThrown();
return new String[] {
String.format("%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp", new Date(r.getMillis())),
source,
r.getLevel().getLocalizedName(),
x == null ? message : message + printThrowable(x) + "\n"
};
}
/**
* Reverses a collection so that it can be easily walked in reverse order.
* @since 1.525
*/
public static Iterable reverse(Collection collection) {
List list = new ArrayList<>(collection);
Collections.reverse(list);
return list;
}
public static Cookie getCookie(HttpServletRequest req,String name) {
Cookie[] cookies = req.getCookies();
if(cookies!=null) {
for (Cookie cookie : cookies) {
if(cookie.getName().equals(name)) {
return cookie;
}
}
}
return null;
}
public static String getCookie(HttpServletRequest req,String name, String defaultValue) {
Cookie c = getCookie(req, name);
if(c==null || c.getValue()==null) return defaultValue;
return c.getValue();
}
private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+");
@Restricted(NoExternalUse.class)
public static String validateIconSize(String iconSize) throws SecurityException {
if (!ICON_SIZE.matcher(iconSize).matches()) {
throw new SecurityException("invalid iconSize");
}
return iconSize;
}
/**
* Gets the suffix to use for YUI JavaScript.
*/
public static String getYuiSuffix() {
return DEBUG_YUI ? "debug" : "min";
}
/**
* Set to true if you need to use the debug version of YUI.
*/
public static boolean DEBUG_YUI = SystemProperties.getBoolean("debug.YUI");
/**
* Creates a sub map by using the given range (both ends inclusive).
*/
public static SortedMap filter(SortedMap map, String from, String to) {
if(from==null && to==null) return map;
if(to==null)
return map.headMap(Integer.parseInt(from)-1);
if(from==null)
return map.tailMap(Integer.parseInt(to));
return map.subMap(Integer.parseInt(to),Integer.parseInt(from)-1);
}
/**
* Creates a sub map by using the given range (upper end inclusive).
*/
@Restricted(NoExternalUse.class)
public static SortedMap filterExcludingFrom(SortedMap map, String from, String to) {
if(from==null && to==null) return map;
if(to==null)
return map.headMap(Integer.parseInt(from));
if(from==null)
return map.tailMap(Integer.parseInt(to));
return map.subMap(Integer.parseInt(to),Integer.parseInt(from));
}
private static final SimpleFormatter formatter = new SimpleFormatter();
/**
* Used by {@code layout.jelly} to control the auto refresh behavior.
*
* @param noAutoRefresh
* On certain pages, like a page with forms, will have annoying interference
* with auto refresh. On those pages, disable auto-refresh.
*/
public static void configureAutoRefresh(HttpServletRequest request, HttpServletResponse response, boolean noAutoRefresh) {
if(noAutoRefresh)
return;
String param = request.getParameter("auto_refresh");
boolean refresh = isAutoRefresh(request);
if (param != null) {
refresh = Boolean.parseBoolean(param);
Cookie c = new Cookie("hudson_auto_refresh", Boolean.toString(refresh));
// Need to set path or it will not stick from e.g. a project page to the dashboard.
// Using request.getContextPath() might work but it seems simpler to just use the hudson_ prefix
// to avoid conflicts with any other web apps that might be on the same machine.
c.setPath("/");
c.setMaxAge(60*60*24*30); // persist it roughly for a month
c.setHttpOnly(true);
response.addCookie(c);
}
if (refresh) {
response.addHeader("Refresh", SystemProperties.getString("hudson.Functions.autoRefreshSeconds", "10"));
}
try {
ExtensionList.lookupSingleton(AutoRefresh.class).recordRequest(request, refresh);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to record auto refresh status in telemetry", e);
}
}
public static boolean isAutoRefresh(HttpServletRequest request) {
String param = request.getParameter("auto_refresh");
if (param != null) {
return Boolean.parseBoolean(param);
}
Cookie[] cookies = request.getCookies();
if(cookies==null)
return false; // when API design messes it up, we all suffer
for (Cookie c : cookies) {
if (c.getName().equals("hudson_auto_refresh")) {
return Boolean.parseBoolean(c.getValue());
}
}
return false;
}
public static boolean isCollapsed(String paneId) {
return PaneStatusProperties.forCurrentUser().isCollapsed(paneId);
}
@Restricted(NoExternalUse.class)
public static boolean isUserTimeZoneOverride() {
return TimeZoneProperty.forCurrentUser() != null;
}
@CheckForNull
@Restricted(NoExternalUse.class)
public static String getUserTimeZone() {
return TimeZoneProperty.forCurrentUser();
}
@Restricted(NoExternalUse.class)
public static String getUserTimeZonePostfix() {
if (!isUserTimeZoneOverride()) {
return "";
}
TimeZone tz = TimeZone.getTimeZone(getUserTimeZone());
return tz.getDisplayName(tz.observesDaylightTime(), TimeZone.SHORT);
}
/**
* Finds the given object in the ancestor list and returns its URL.
* This is used to determine the "current" URL assigned to the given object,
* so that one can compute relative URLs from it.
*/
public static String getNearestAncestorUrl(StaplerRequest req,Object it) {
List list = req.getAncestors();
for( int i=list.size()-1; i>=0; i-- ) {
Ancestor anc = (Ancestor) list.get(i);
if(anc.getObject()==it)
return anc.getUrl();
}
return null;
}
/**
* Finds the inner-most {@link SearchableModelObject} in scope.
*/
public static String getSearchURL() {
List list = Stapler.getCurrentRequest().getAncestors();
for( int i=list.size()-1; i>=0; i-- ) {
Ancestor anc = (Ancestor) list.get(i);
if(anc.getObject() instanceof SearchableModelObject)
return anc.getUrl()+"/search/";
}
return null;
}
public static String appendSpaceIfNotNull(String n) {
if(n==null) return null;
else return n+' ';
}
/**
* One nbsp per 10 pixels in given size, which may be a plain number or "NxN"
* (like an iconSize). Useful in a sortable table heading.
*/
public static String nbspIndent(String size) {
int i = size.indexOf('x');
i = Integer.parseInt(i > 0 ? size.substring(0, i) : size) / 10;
StringBuilder buf = new StringBuilder(30);
for (int j = 0; j < i; j++)
buf.append(" ");
return buf.toString();
}
public static String getWin32ErrorMessage(IOException e) {
return Util.getWin32ErrorMessage(e);
}
public static boolean isMultiline(String s) {
if(s==null) return false;
return s.indexOf('\r')>=0 || s.indexOf('\n')>=0;
}
public static String encode(String s) {
return Util.encode(s);
}
/**
* Shortcut function for calling {@link URLEncoder#encode(String,String)} (with UTF-8 encoding).
* Useful for encoding URL query parameters in jelly code (as in {@code "...?param=${h.urlEncode(something)}"}).
* For convenience in jelly code, it also accepts null parameter, and then returns an empty string.
*
* @since 2.200
*/
public static String urlEncode(String s) {
if (s == null) {
return "";
}
try {
return URLEncoder.encode(s, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
}
}
public static String escape(String s) {
return Util.escape(s);
}
public static String xmlEscape(String s) {
return Util.xmlEscape(s);
}
public static String xmlUnescape(String s) {
return s.replace("<","<").replace(">",">").replace("&","&");
}
public static String htmlAttributeEscape(String text) {
StringBuilder buf = new StringBuilder(text.length()+64);
for( int i=0; i')
buf.append(">");
else
if(ch=='&')
buf.append("&");
else
if(ch=='"')
buf.append(""");
else
if(ch=='\'')
buf.append("'");
else
buf.append(ch);
}
return buf.toString();
}
public static void checkPermission(Permission permission) throws IOException, ServletException {
checkPermission(Jenkins.get(),permission);
}
public static void checkPermission(AccessControlled object, Permission permission) throws IOException, ServletException {
if (permission != null) {
object.checkPermission(permission);
}
}
/**
* This version is so that the 'checkPermission' on {@code layout.jelly}
* degrades gracefully if "it" is not an {@link AccessControlled} object.
* Otherwise it will perform no check and that problem is hard to notice.
*/
public static void checkPermission(Object object, Permission permission) throws IOException, ServletException {
if (permission == null)
return;
if (object instanceof AccessControlled)
checkPermission((AccessControlled) object,permission);
else {
List ancs = Stapler.getCurrentRequest().getAncestors();
for(Ancestor anc : Iterators.reverse(ancs)) {
Object o = anc.getObject();
if (o instanceof AccessControlled) {
checkPermission((AccessControlled) o,permission);
return;
}
}
checkPermission(Jenkins.get(),permission);
}
}
/**
* Returns true if the current user has the given permission.
*
* @param permission
* If null, returns true. This defaulting is convenient in making the use of this method terse.
*/
public static boolean hasPermission(Permission permission) throws IOException, ServletException {
return hasPermission(Jenkins.get(),permission);
}
/**
* This version is so that the 'hasPermission' can degrade gracefully
* if "it" is not an {@link AccessControlled} object.
*/
public static boolean hasPermission(Object object, Permission permission) throws IOException, ServletException {
if (permission == null)
return true;
if (object instanceof AccessControlled)
return ((AccessControlled)object).hasPermission(permission);
else {
List ancs = Stapler.getCurrentRequest().getAncestors();
for(Ancestor anc : Iterators.reverse(ancs)) {
Object o = anc.getObject();
if (o instanceof AccessControlled) {
return ((AccessControlled)o).hasPermission(permission);
}
}
return Jenkins.get().hasPermission(permission);
}
}
public static void adminCheck(StaplerRequest req, StaplerResponse rsp, Object required, Permission permission) throws IOException, ServletException {
// this is legacy --- all views should be eventually converted to
// the permission based model.
if(required!=null && !Hudson.adminCheck(req, rsp)) {
// check failed. commit the FORBIDDEN response, then abort.
rsp.setStatus(HttpServletResponse.SC_FORBIDDEN);
rsp.getOutputStream().close();
throw new ServletException("Unauthorized access");
}
// make sure the user owns the necessary permission to access this page.
if(permission!=null)
checkPermission(permission);
}
/**
* Infers the hudson installation URL from the given request.
*/
public static String inferHudsonURL(StaplerRequest req) {
String rootUrl = Jenkins.get().getRootUrl();
if(rootUrl !=null)
// prefer the one explicitly configured, to work with load-balancer, frontend, etc.
return rootUrl;
StringBuilder buf = new StringBuilder();
buf.append(req.getScheme()).append("://");
buf.append(req.getServerName());
if(! (req.getScheme().equals("http") && req.getLocalPort()==80 || req.getScheme().equals("https") && req.getLocalPort()==443))
buf.append(':').append(req.getLocalPort());
buf.append(req.getContextPath()).append('/');
return buf.toString();
}
/**
* Returns the link to be displayed in the footer of the UI.
*/
public static String getFooterURL() {
if(footerURL == null) {
footerURL = SystemProperties.getString("hudson.footerURL");
if(StringUtils.isBlank(footerURL)) {
footerURL = "https://jenkins.io/";
}
}
return footerURL;
}
private static String footerURL = null;
public static List getJobPropertyDescriptors(Class extends Job> clazz) {
return JobPropertyDescriptor.getPropertyDescriptors(clazz);
}
public static List getJobPropertyDescriptors(Job job) {
return DescriptorVisibilityFilter.apply(job, JobPropertyDescriptor.getPropertyDescriptors(job.getClass()));
}
public static List> getBuildWrapperDescriptors(AbstractProject,?> project) {
return BuildWrappers.getFor(project);
}
public static List> getSecurityRealmDescriptors() {
return SecurityRealm.all();
}
public static List> getAuthorizationStrategyDescriptors() {
return AuthorizationStrategy.all();
}
public static List> getBuilderDescriptors(AbstractProject,?> project) {
return BuildStepDescriptor.filter(Builder.all(), project.getClass());
}
public static List> getPublisherDescriptors(AbstractProject,?> project) {
return BuildStepDescriptor.filter(Publisher.all(), project.getClass());
}
public static List> getSCMDescriptors(AbstractProject,?> project) {
return SCM._for(project);
}
/**
* @since 2.12
* @deprecated replaced by {@link Slave.SlaveDescriptor#computerLauncherDescriptors(Slave)}
*/
@Deprecated
@Restricted(DoNotUse.class)
@RestrictedSince("2.12")
public static List> getComputerLauncherDescriptors() {
return Jenkins.get().>getDescriptorList(ComputerLauncher.class);
}
/**
* @since 2.12
* @deprecated replaced by {@link Slave.SlaveDescriptor#retentionStrategyDescriptors(Slave)}
*/
@Deprecated
@Restricted(DoNotUse.class)
@RestrictedSince("2.12")
public static List>> getRetentionStrategyDescriptors() {
return RetentionStrategy.all();
}
public static List getParameterDescriptors() {
return ParameterDefinition.all();
}
public static List> getCaptchaSupportDescriptors() {
return CaptchaSupport.all();
}
public static List> getViewsTabBarDescriptors() {
return ViewsTabBar.all();
}
public static List> getMyViewsTabBarDescriptors() {
return MyViewsTabBar.all();
}
/**
* @deprecated replaced by {@link Slave.SlaveDescriptor#nodePropertyDescriptors(Slave)}
* @since 2.12
*/
@Deprecated
@Restricted(DoNotUse.class)
@RestrictedSince("2.12")
public static List getNodePropertyDescriptors(Class extends Node> clazz) {
List result = new ArrayList<>();
Collection list = (Collection) Jenkins.get().getDescriptorList(NodeProperty.class);
for (NodePropertyDescriptor npd : list) {
if (npd.isApplicable(clazz)) {
result.add(npd);
}
}
return result;
}
/**
* Returns those node properties which can be configured as global node properties.
*
* @since 1.520
*/
public static List getGlobalNodePropertyDescriptors() {
List result = new ArrayList<>();
Collection list = (Collection) Jenkins.get().getDescriptorList(NodeProperty.class);
for (NodePropertyDescriptor npd : list) {
if (npd.isApplicableAsGlobal()) {
result.add(npd);
}
}
return result;
}
/**
* Gets all the descriptors sorted by their inheritance tree of {@link Describable}
* so that descriptors of similar types come nearby.
*
*
* We sort them by {@link Extension#ordinal()} but only for {@link GlobalConfiguration}s,
* as the value is normally used to compare similar kinds of extensions, and we needed
* {@link GlobalConfiguration}s to be able to position themselves in a layer above.
* This however creates some asymmetry between regular {@link Descriptor}s and {@link GlobalConfiguration}s.
* Perhaps it is better to introduce another annotation element? But then,
* extensions shouldn't normally concern themselves about ordering too much, and the only reason
* we needed this for {@link GlobalConfiguration}s are for backward compatibility.
*
* @param predicate
* Filter the descriptors based on {@link GlobalConfigurationCategory}
* @since 1.494
*/
public static Collection getSortedDescriptorsForGlobalConfig(Predicate predicate) {
ExtensionList exts = ExtensionList.lookup(Descriptor.class);
List r = new ArrayList<>(exts.size());
for (ExtensionComponent c : exts.getComponents()) {
Descriptor d = c.getInstance();
if (d.getGlobalConfigPage()==null) continue;
if (predicate.apply(d.getCategory())) {
r.add(new Tag(c.ordinal(), d));
}
}
Collections.sort(r);
List answer = new ArrayList<>(r.size());
for (Tag d : r) answer.add(d.d);
return DescriptorVisibilityFilter.apply(Jenkins.get(),answer);
}
/**
* Like {@link #getSortedDescriptorsForGlobalConfig(Predicate)} but with a constant truth predicate, to include all descriptors.
*/
public static Collection getSortedDescriptorsForGlobalConfig() {
return getSortedDescriptorsForGlobalConfig(Predicates.alwaysTrue());
}
/**
* @deprecated This is rather meaningless.
*/
@Deprecated
public static Collection getSortedDescriptorsForGlobalConfigNoSecurity() {
return getSortedDescriptorsForGlobalConfig(Predicates.not(GlobalSecurityConfiguration.FILTER));
}
/**
* Like {@link #getSortedDescriptorsForGlobalConfig(Predicate)} but for unclassified descriptors only.
* @since 1.506
*/
public static Collection getSortedDescriptorsForGlobalConfigUnclassified() {
return getSortedDescriptorsForGlobalConfig(new Predicate() {
public boolean apply(GlobalConfigurationCategory cat) {
return cat instanceof GlobalConfigurationCategory.Unclassified;
}
});
}
private static class Tag implements Comparable {
double ordinal;
String hierarchy;
Descriptor d;
Tag(double ordinal, Descriptor d) {
this.ordinal = ordinal;
this.d = d;
this.hierarchy = buildSuperclassHierarchy(d.clazz, new StringBuilder()).toString();
}
private StringBuilder buildSuperclassHierarchy(Class c, StringBuilder buf) {
Class sc = c.getSuperclass();
if (sc!=null) buildSuperclassHierarchy(sc,buf).append(':');
return buf.append(c.getName());
}
public int compareTo(Tag that) {
int r = Double.compare(that.ordinal, this.ordinal);
if (r!=0) return r; // descending for ordinal by reversing the order for compare
return this.hierarchy.compareTo(that.hierarchy);
}
}
/**
* Computes the path to the icon of the given action
* from the context path.
*/
public static String getIconFilePath(Action a) {
String name = a.getIconFileName();
if (name==null) return null;
if (name.startsWith("/"))
return name.substring(1);
else
return "images/24x24/"+name;
}
/**
* Works like JSTL build-in size(x) function,
* but handle null gracefully.
*/
public static int size2(Object o) throws Exception {
if(o==null) return 0;
return ASTSizeFunction.sizeOf(o,Introspector.getUberspect());
}
/**
* Computes the relative path from the current page to the given item.
*/
public static String getRelativeLinkTo(Item p) {
Map