/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt * * 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.model; import hudson.FilePath; import hudson.Util; import hudson.util.IOException2; import jenkins.model.Jenkins; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.HttpResponse; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.StringTokenizer; import java.util.logging.Logger; import java.util.logging.Level; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import jenkins.util.VirtualFile; /** * Has convenience methods to serve file system. * *
* This object can be used in a mix-in style to provide a directory browsing capability
* to a {@link ModelObject}.
*
* @author Kohsuke Kawaguchi
*/
public final class DirectoryBrowserSupport implements HttpResponse {
public final ModelObject owner;
public final String title;
private final VirtualFile base;
private final String icon;
private final boolean serveDirIndex;
private String indexFileName = "index.html";
/**
* @deprecated as of 1.297
* Use {@link #DirectoryBrowserSupport(ModelObject, FilePath, String, String, boolean)}
*/
public DirectoryBrowserSupport(ModelObject owner, String title) {
this(owner, (VirtualFile) null, title, null, false);
}
/**
* @param owner
* The parent model object under which the directory browsing is added.
* @param base
* The root of the directory that's bound to URL.
* @param title
* Used in the HTML caption.
* @param icon
* The icon file name, like "folder.gif"
* @param serveDirIndex
* True to generate the directory index.
* False to serve "index.html"
*/
public DirectoryBrowserSupport(ModelObject owner, FilePath base, String title, String icon, boolean serveDirIndex) {
this(owner, base.toVirtualFile(), title, icon, serveDirIndex);
}
/**
* @param owner
* The parent model object under which the directory browsing is added.
* @param base
* The root of the directory that's bound to URL.
* @param title
* Used in the HTML caption.
* @param icon
* The icon file name, like "folder.gif"
* @param serveDirIndex
* True to generate the directory index.
* False to serve "index.html"
* @since 1.532
*/
public DirectoryBrowserSupport(ModelObject owner, VirtualFile base, String title, String icon, boolean serveDirIndex) {
this.owner = owner;
this.base = base;
this.title = title;
this.icon = icon;
this.serveDirIndex = serveDirIndex;
}
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
try {
serveFile(req,rsp,base,icon,serveDirIndex);
} catch (InterruptedException e) {
throw new IOException2("interrupted",e);
}
}
/**
* If the directory is requested but the directory listing is disabled, a file of this name
* is served. By default it's "index.html".
* @since 1.312
*/
public void setIndexFileName(String fileName) {
this.indexFileName = fileName;
}
/**
* Serves a file from the file system (Maps the URL to a directory in a file system.)
*
* @param icon
* The icon file name, like "folder-open.gif"
* @param serveDirIndex
* True to generate the directory index.
* False to serve "index.html"
* @deprecated as of 1.297
* Instead of calling this method explicitly, just return the {@link DirectoryBrowserSupport} object
* from the {@code doXYZ} method and let Stapler generate a response for you.
*/
public void serveFile(StaplerRequest req, StaplerResponse rsp, FilePath root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException {
serveFile(req, rsp, root.toVirtualFile(), icon, serveDirIndex);
}
private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException {
// handle form submission
String pattern = req.getParameter("pattern");
if(pattern==null)
pattern = req.getParameter("path"); // compatibility with Hudson<1.129
if(pattern!=null && !Util.isAbsoluteUri(pattern)) {// avoid open redirect
rsp.sendRedirect2(pattern);
return;
}
String path = getPath(req);
if(path.replace('\\','/').indexOf("/../")!=-1) {
// don't serve anything other than files in the artifacts dir
rsp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// split the path to the base directory portion "abc/def/ghi" which doesn't include any wildcard,
// and the GLOB portion "**/*.xml" (the rest)
StringBuilder _base = new StringBuilder();
StringBuilder _rest = new StringBuilder();
int restSize=-1; // number of ".." needed to go back to the 'base' level.
boolean zip=false; // if we are asked to serve a zip file bundle
boolean plain = false; // if asked to serve a plain text directory listing
{
boolean inBase = true;
StringTokenizer pathTokens = new StringTokenizer(path,"/");
while(pathTokens.hasMoreTokens()) {
String pathElement = pathTokens.nextToken();
// Treat * and ? as wildcard unless they match a literal filename
if((pathElement.contains("?") || pathElement.contains("*"))
&& inBase && !root.child((_base.length() > 0 ? _base + "/" : "") + pathElement).exists())
inBase = false;
if(pathElement.equals("*zip*")) {
// the expected syntax is foo/bar/*zip*/bar.zip
// the last 'bar.zip' portion is to causes browses to set a good default file name.
// so the 'rest' portion ends here.
zip=true;
break;
}
if(pathElement.equals("*plain*")) {
plain = true;
break;
}
StringBuilder sb = inBase?_base:_rest;
if(sb.length()>0) sb.append('/');
sb.append(pathElement);
if(!inBase)
restSize++;
}
}
restSize = Math.max(restSize,0);
String base = _base.toString();
String rest = _rest.toString();
// this is the base file/directory
VirtualFile baseFile = root.child(base);
if(baseFile.isDirectory()) {
if(zip) {
rsp.setContentType("application/zip");
zip(rsp.getOutputStream(), baseFile, rest);
return;
}
if (plain) {
rsp.setContentType("text/plain;charset=UTF-8");
OutputStream os = rsp.getOutputStream();
try {
for (VirtualFile kid : baseFile.list()) {
os.write(kid.getName().getBytes("UTF-8"));
if (kid.isDirectory()) {
os.write('/');
}
os.write('\n');
}
os.flush();
} finally {
os.close();
}
return;
}
if(rest.length()==0) {
// if the target page to be displayed is a directory and the path doesn't end with '/', redirect
StringBuffer reqUrl = req.getRequestURL();
if(reqUrl.charAt(reqUrl.length()-1)!='/') {
rsp.sendRedirect2(reqUrl.append('/').toString());
return;
}
}
List> glob = null;
if(rest.length()>0) {
// the rest is Ant glob pattern
glob = patternScan(baseFile, rest, createBackRef(restSize));
} else
if(serveDirIndex) {
// serve directory index
glob = buildChildPaths(baseFile, req.getLocale());
}
if(glob!=null) {
// serve glob
req.setAttribute("it", this);
List
> buildChildPaths(VirtualFile cur, Locale locale) throws IOException {
List
> r = new ArrayList
>();
VirtualFile[] files = cur.list();
Arrays.sort(files,new FileComparator(locale));
for( VirtualFile f : files ) {
Path p = new Path(Util.rawEncode(f.getName()), f.getName(), f.isDirectory(), f.length(), f.canRead());
if(!f.isDirectory()) {
r.add(Collections.singletonList(p));
} else {
// find all empty intermediate directory
List
> patternScan(VirtualFile baseDir, String pattern, String baseRef) throws IOException {
String[] files = baseDir.list(pattern);
if (files.length > 0) {
List
> r = new ArrayList
>(files.length);
for (String match : files) {
List