package jenkins.model;
import hudson.Extension;
import hudson.model.UnprotectedRootAction;
import hudson.util.TimeUnit2;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Enumeration;
/**
* Serves files located in the {@code /assets} classpath directory via the Jenkins core ClassLoader.
* e.g. the URL {@code /assets/jquery-detached/jsmodules/jquery2.js} will load {@code jquery-detached/jsmodules/jquery2.js}
* resource from the classpath below {@code /assets}.
*
* @author Kohsuke Kawaguchi
*
* @since 2.0
*/
@Extension @Symbol("assetManager")
public class AssetManager implements UnprotectedRootAction {
// not shown in the UI
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return "assets";
}
/**
* Exposes assets in the core classloader over HTTP.
*/
public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
String path = req.getRestOfPath();
URL resource = findResource(path);
if (resource == null) {
rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Stapler routes requests like the "/static/.../foo/bar/zot" to be treated like "/foo/bar/zot"
// and this is used to serve long expiration header, by using Jenkins.VERSION_HASH as "..."
// to create unique URLs. Recognize that and set a long expiration header.
String requestPath = req.getRequestURI().substring(req.getContextPath().length());
boolean staticLink = requestPath.startsWith("/static/");
long expires = staticLink ? TimeUnit2.DAYS.toMillis(365) : -1;
// use serveLocalizedFile to support automatic locale selection
rsp.serveLocalizedFile(req, resource, expires);
}
/**
* Locates the asset from the classloader.
*
* <p>
* To allow plugins to bring its own assets without worrying about colliding with the assets in core,
* look for child classloader first. But to support plugins that get split, if the child classloader
* doesn't find it, fall back to the parent classloader.
*/
private URL findResource(String path) throws IOException {
try {
if (path.contains("..")) // crude avoidance of directory traversal attack
throw new IllegalArgumentException(path);
String name;
if (path.charAt(0) == '/') {
name = "assets" + path;
} else {
name = "assets/" + path;
}
ClassLoader cl = Jenkins.class.getClassLoader();
URL url = (URL) $findResource.invoke(cl, name);
if (url==null) {
// pick the last one, which is the one closest to the leaf of the classloader tree.
Enumeration<URL> e = cl.getResources(name);
while (e.hasMoreElements()) {
url = e.nextElement();
}
}
return url;
} catch (InvocationTargetException|IllegalAccessException e) {
throw new Error(e);
}
}
private static final Method $findResource = init();
private static Method init() {
try {
Method m = ClassLoader.class.getDeclaredMethod("findResource", String.class);
m.setAccessible(true);
return m;
} catch (NoSuchMethodException e) {
throw (Error)new NoSuchMethodError().initCause(e);
}
}
}