Security context
Medium· 4.3GHSA-cj6r-8pxj-5jv6 CVE-2023-27902Published Mar 10, 2023

Incorrect Permission Preservation in Jenkins Core

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

2.376 → fixed in 2.387.10 → fixed in 2.375.42.388 → fixed in 2.394

Details

Jenkins uses temporary directories adjacent to workspace directories, usually with the @tmp name suffix, to store temporary files related to the build. In pipelines, these temporary directories are adjacent to the current working directory when operating in a subdirectory of the automatically allocated workspace. Jenkins-controlled processes, like SCMs, may store credentials in these directories. Jenkins 2.393 and earlier, LTS 2.375.3 and earlier, and prior to LTS 2.387.1 shows these temporary directories when viewing job workspaces, which allows attackers with Item/Workspace permission to access their contents. Jenkins 2.394, LTS 2.375.4, and LTS 2.387.1 does not list these temporary directories in job workspaces. As a workaround, do not grant Item/Workspace permission to users who lack Item/Configure permission, if you’re concerned about this issue but unable to immediately update Jenkins.

The fix

[SECURITY-1807]

Jeff Thompson· Feb 23, 2023, 07:58 AM+27913980452662b3
core/src/main/java/hudson/FilePath.java+135 49
@@ -26,7 +26,6 @@
package hudson;
-import static hudson.FilePath.TarCompression.GZIP;
import static hudson.Util.fileToPath;
import static hudson.Util.fixEmpty;
@@ -87,11 +86,13 @@
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
+import java.nio.file.CopyOption;
import java.nio.file.FileSystemException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
@@ -115,6 +116,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
@@ -210,6 +212,11 @@
* @see VirtualFile
*/
public final class FilePath implements SerializableOnlyOverRemoting {
+
+ public enum DisplayOption implements OpenOption, CopyOption {
+ IGNORE_TMP_DIRS
+ }
+
/**
* Maximum http redirects we will follow. This defaults to the same number as Firefox/Chrome tolerates.
*/
@@ -469,17 +476,16 @@ public int zip(OutputStream out, DirScanner scanner) throws IOException, Interru
* @param verificationRoot A root or base directory for checking for any symlinks in this files parentage.
* Any symlinks between a file and root should be ignored.
* Symlinks in the parentage outside root will not be checked.
- * @param noFollowLinks true if it should not follow links.
* @param prefix The portion of file path that will be added at the beginning of the relative path inside the archive.
* If non-empty, a trailing forward slash will be enforced.
- *
+ * @param openOptions the options to apply when opening.
* @return The number of files/directories archived.
* This is only really useful to check for a situation where nothing
*/
@Restricted(NoExternalUse.class)
- public int zip(OutputStream out, DirScanner scanner, String verificationRoot, boolean noFollowLinks, String prefix) throws IOException, InterruptedException {
- ArchiverFactory archiverFactory = noFollowLinks ? ArchiverFactory.createZipWithoutSymlink(prefix) : ArchiverFactory.ZIP;
- return archive(archiverFactory, out, scanner, verificationRoot, noFollowLinks);
+ public int zip(OutputStream out, DirScanner scanner, String verificationRoot, String prefix, OpenOption... openOptions) throws IOException, InterruptedException {
+ ArchiverFactory archiverFactory = prefix == null ? ArchiverFactory.ZIP : ArchiverFactory.createZipWithPrefix(prefix, openOptions);
+ return archive(archiverFactory, out, scanner, verificationRoot, openOptions);
}
/**
@@ -491,7 +497,7 @@ public int zip(OutputStream out, DirScanner scanner, String verificationRoot, bo
* is archived.
*/
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException {
- return archive(factory, os, scanner, null, false);
+ return archive(factory, os, scanner, null);
}
/**
@@ -503,16 +509,16 @@ public int archive(final ArchiverFactory factory, OutputStream os, final DirScan
* @param verificationRoot A root or base directory for checking for any symlinks in this files parentage.
* Any symlinks between a file and root should be ignored.
* Symlinks in the parentage outside root will not be checked.
- * @param noFollowLinks true if it should not follow links.
+ * @param openOptions options to apply when opening.
*
* @return The number of files/directories archived.
* This is only really useful to check for a situation where nothing
*/
@Restricted(NoExternalUse.class)
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner,
- String verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
+ String verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException {
final OutputStream out = channel != null ? new RemoteOutputStream(os) : os;
- return act(new Archive(factory, out, scanner, verificationRoot, noFollowLinks));
+ return act(new Archive(factory, out, scanner, verificationRoot, openOptions));
}
private static class Archive extends MasterToSlaveFileCallable<Integer> {
@@ -520,20 +526,20 @@ private static class Archive extends MasterToSlaveFileCallable<Integer> {
private final OutputStream out;
private final DirScanner scanner;
private final String verificationRoot;
- private final boolean noFollowLinks;
+ private OpenOption[] openOptions;
- Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, boolean noFollowLinks) {
+ Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, OpenOption... openOptions) {
this.factory = factory;
this.out = out;
this.scanner = scanner;
this.verificationRoot = verificationRoot;
- this.noFollowLinks = noFollowLinks;
+ this.openOptions = openOptions;
}
@Override
public Integer invoke(File f, VirtualChannel channel) throws IOException {
try (Archiver a = factory.create(out)) {
- scanner.scan(f, ignoringSymlinks(a, verificationRoot, noFollowLinks));
+ scanner.scan(f, ignoringTmpDirs(ignoringSymlinks(a, verificationRoot, openOptions), verificationRoot, openOptions));
return a.countEntries();
}
}
@@ -756,44 +762,44 @@ public String invoke(File f, VirtualChannel channel) throws IOException {
}
@Restricted(NoExternalUse.class)
- public boolean hasSymlink(FilePath verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
- return act(new HasSymlink(verificationRoot == null ? null : verificationRoot.remote, noFollowLinks));
+ public boolean hasSymlink(FilePath verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException {
+ return act(new HasSymlink(verificationRoot == null ? null : verificationRoot.remote, openOptions));
}
private static class HasSymlink extends MasterToSlaveFileCallable<Boolean> {
private static final long serialVersionUID = 1L;
private final String verificationRoot;
- private final boolean noFollowLinks;
+ private OpenOption[] openOptions;
- HasSymlink(String verificationRoot, boolean noFollowLinks) {
+ HasSymlink(String verificationRoot, OpenOption... openOptions) {
this.verificationRoot = verificationRoot;
- this.noFollowLinks = noFollowLinks;
+ this.openOptions = openOptions;
}
@Override
public Boolean invoke(File f, VirtualChannel channel) throws IOException {
- return isSymlink(f, verificationRoot, noFollowLinks);
+ return isSymlink(f, verificationRoot, openOptions);
}
}
@Restricted(NoExternalUse.class)
- public boolean containsSymlink(FilePath verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
- return !list(new SymlinkRetainingFileFilter(verificationRoot, noFollowLinks)).isEmpty();
+ public boolean containsSymlink(FilePath verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException {
+ return !list(new SymlinkRetainingFileFilter(verificationRoot, openOptions)).isEmpty();
}
private static class SymlinkRetainingFileFilter implements FileFilter, Serializable {
private final String verificationRoot;
- private final boolean noFollowLinks;
+ private OpenOption[] openOptions;
- SymlinkRetainingFileFilter(FilePath verificationRoot, boolean noFollowLinks) {
+ SymlinkRetainingFileFilter(FilePath verificationRoot, OpenOption... openOptions) {
this.verificationRoot = verificationRoot == null ? null : verificationRoot.remote;
- this.noFollowLinks = noFollowLinks;
+ this.openOptions = openOptions;
}
@Override
public boolean accept(File file) {
- return isSymlink(file, verificationRoot, noFollowLinks);
+ return isSymlink(file, verificationRoot, openOptions);
}
private static final long serialVersionUID = 1L;
@@ -1050,7 +1056,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen
if (archive.toExternalForm().endsWith(".zip"))
unzipFrom(cis);
else
- untarFrom(cis, GZIP);
+ untarFrom(cis, TarCompression.GZIP);
} catch (IOException e) {
throw new IOException(String.format("Failed to unpack %s (%d bytes read of total %d)",
archive, cis.getByteCount(), con.getContentLength()), e);
@@ -1077,7 +1083,7 @@ private static final class Unpack extends MasterToSlaveFileCallable<Void> {
if (archive.toExternalForm().endsWith(".zip")) {
unzip(dir, cis);
} else {
- readFromTar("input stream", dir, GZIP.extract(cis));
+ readFromTar("input stream", dir, TarCompression.GZIP.extract(cis));
}
} catch (IOException x) {
throw new IOException(String.format("Failed to unpack %s (%d bytes read)", archive, cis.getByteCount()), x);
@@ -2010,13 +2016,13 @@ public List<FilePath> list() throws IOException, InterruptedException {
* @param verificationRoot A root or base directory for checking for any symlinks in this files parentage.
* Any symlinks between a file and root should be ignored.
* Symlinks in the parentage outside root will not be checked.
- * @param noFollowLinks true if it should not follow links.
+ * @param openOptions the options to apply when opening.
* @return Direct children of this directory.
*/
@Restricted(NoExternalUse.class)
@NonNull
- public List<FilePath> list(FilePath verificationRoot, boolean noFollowLinks) throws IOException, InterruptedException {
- return list(new SymlinkDiscardingFileFilter(verificationRoot, noFollowLinks));
+ public List<FilePath> list(FilePath verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException {
+ return list(new OptionalDiscardingFileFilter(verificationRoot, openOptions));
}
/**
@@ -2175,31 +2181,32 @@ private static String[] glob(File dir, String includes, String excludes, boolean
* Reads this file.
*/
public InputStream read() throws IOException, InterruptedException {
- return read(null, false);
+ return read(null, new OpenOption[0]);
}
@Restricted(NoExternalUse.class)
- public InputStream read(FilePath rootPath, boolean noFollowLinks) throws IOException, InterruptedException {
+ public InputStream read(FilePath rootPath, OpenOption... openOptions) throws IOException, InterruptedException {
String rootPathString = rootPath == null ? null : rootPath.remote;
if (channel == null) {
File file = new File(remote);
- InputStream inputStream = newInputStreamDenyingSymlinkAsNeeded(file, rootPathString, noFollowLinks);
+ InputStream inputStream = newInputStreamDenyingSymlinkAsNeeded(file, rootPathString, openOptions);
return inputStream;
}
final Pipe p = Pipe.createRemoteToLocal();
- actAsync(new Read(p, rootPathString, noFollowLinks));
+ actAsync(new Read(p, rootPathString, openOptions));
return p.getIn();
}
@Restricted(NoExternalUse.class)
- public static InputStream newInputStreamDenyingSymlinkAsNeeded(File file, String verificationRoot, boolean noFollowLinks) throws IOException {
+ public static InputStream newInputStreamDenyingSymlinkAsNeeded(File file, String verificationRoot, OpenOption... openOptions) throws IOException {
InputStream inputStream = null;
try {
- denySymlink(file, verificationRoot, noFollowLinks);
- inputStream = noFollowLinks ? Files.newInputStream(fileToPath(file), LinkOption.NOFOLLOW_LINKS) : Files.newInputStream(fileToPath(file));
- denySymlink(file, verificationRoot, noFollowLinks);
+ denyTmpDir(file, verificationRoot, openOptions);
+ denySymlink(file, verificationRoot, openOptions);
+ inputStream = openInputStream(file, openOptions);
+ denySymlink(file, verificationRoot, openOptions);
} catch (IOException ioe) {
if (inputStream != null) {
inputStream.close();
@@ -2209,7 +2216,19 @@ public static InputStream newInputStreamDenyingSymlinkAsNeeded(File file, String
return inputStream;
}
- private static void denySymlink(File file, String root, boolean noFollowLinks) throws IOException {
+ @Restricted(NoExternalUse.class)
+ public static InputStream openInputStream(File file, OpenOption[] openOptions) throws IOException {
+ return Files.newInputStream(fileToPath(file), stripLocalOptions(openOptions));
+ }
+
+ private static OpenOption[] stripLocalOptions(OpenOption... openOptions) {
+ if (openOptions != null) {
+ return Arrays.stream(openOptions).filter(option -> option != DisplayOption.IGNORE_TMP_DIRS).toArray(OpenOption[]::new);
+ }
+ return null;
+ }
+
+ private static void denySymlink(File file, String root, OpenOption... openOptions) throws IOException {
/* This should be checked right before the file is opened or otherwise traversed.
If at all possible, it should also be checked immediately afterwards.
This narrows any possible race conditions that may exist in weird situations,
@@ -2222,14 +2241,20 @@ private static void denySymlink(File file, String root, boolean noFollowLinks) t
Files.newInputStream(path, LinkOption.NOFOLLOW_LINKS) implementation.
*/
- if (isSymlink(file, root, noFollowLinks)) {
+ if (isSymlink(file, root, openOptions)) {
throw new IOException("Symlinks are prohibited.");
}
}
+ private static void denyTmpDir(File file, String root, OpenOption... openOptions) throws IOException {
+ if (isTmpDir(file, root, openOptions)) {
+ throw new IOException("Tmp directory is prohibited.");
+ }
+ }
+
@Restricted(NoExternalUse.class)
- public static boolean isSymlink(File file, String root, boolean noFollowLinks) {
- if (noFollowLinks) {
+ public static boolean isSymlink(File file, String root, OpenOption... openOptions) {
+ if (isNoFollowLink(openOptions)) {
if (Util.isSymlink(file.toPath())) {
return true;
}
@@ -2239,6 +2264,67 @@ public static boolean isSymlink(File file, String root, boolean noFollowLinks) {
return false;
}
+ private static boolean isSymlink(VisitorInfo visitorInfo) {
+ return isSymlink(visitorInfo.f, visitorInfo.verificationRoot, visitorInfo.openOptions);
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static boolean isTmpDir(File file, String root, OpenOption... openOptions) {
+ if (isIgnoreTmpDirs(openOptions)) {
+ if (isTmpDir(file)) {
+ return true;
+ }
+
+ return isFileAncestorTmpDir(file, root);
+ }
+ return false;
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static boolean isTmpDir(String filename, OpenOption... openOptions) {
+ if (isIgnoreTmpDirs(openOptions)) {
+ return isTmpDir(filename);
+ }
+ return false;
+ }
+
+ private static boolean isTmpDir(VisitorInfo visitorInfo) {
+ return isTmpDir(visitorInfo.f, visitorInfo.verificationRoot, visitorInfo.openOptions);
+ }
+
+ private static boolean isTmpDir(File file) {
+ return file.isDirectory() && isTmpDir(file.getName());
+ }
+
+ private static boolean isTmpDir(String filename) {
+ return filename.length() > WorkspaceList.TMP_DIR_SUFFIX.length() && filename.endsWith(WorkspaceList.TMP_DIR_SUFFIX);
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static boolean isNoFollowLink(OpenOption... openOptions) {
+ return Arrays.asList(openOptions).contains(LinkOption.NOFOLLOW_LINKS);
+ }
+
+ @Restricted(NoExternalUse.class)
+ public static boolean isIgnoreTmpDirs(OpenOption... openOptions) {
+ return Arrays.asList(openOptions).contains(DisplayOption.IGNORE_TMP_DIRS);
+ }
+
+ private static boolean isFileAncestorSymlink(File file, String root) {
+ return doesFileAncestorMatch(file, root, Util::isSymlink);
+ }
+
+ /**
+ * Determines whether an ancestor of this file is a tmp directory, between the specified
+ * file and the root path. Ancestors further up the tree are not considered.
+ * @param file The base file for the beginning of the search.
+ * @param root The root path for ending the search.
+ * @return True if there is a tmp directory within the domain. False otherwise.
+ */
+ private static boolean isFileAncestorTmpDir(File file, String root) {
+ return doesFileAncestorMatch(file, root, path -> isTmpDir(path.toFile()));
+ }
+
/**
* Determines whether an ancestor of this file is a symlink, between the specified
* file and the root path. Ancestors further up the tree are not considered.
@@ -2246,13 +2332,13 @@ public static boolean isSymlink(File file, String root, boolean noFollowLinks) {
* @param root The root path for ending the search.
* @return True if there is a symlink within the domain. False otherwise.
*/
- private static boolean isFileAncestorSymlink(File file, String root) {
+ private static boolean doesFileAncestorMatch(File file, String root, Predicate<Path> matcher) {
if (root != null) {
Path rootPath = Paths.get(root);
Path currPath = file.toPath();
try {
while (!getRealPath(currPath).equals(getRealPath(rootPath))) {
- if (Util.isSymlink(currPath)) {
+ if (matcher.test(currPath)) {
return true;
}
currPath = currPath.getParent();
@@ -2271,17 +2357,17 @@ private static class Read extends MasterToSlaveFileCallable<Void> {
private static final long serialVersionUID = 1L;
private final Pipe p;
private String verificationRoot;
- private boolean noFollowLinks;
+ private OpenOption[] openOptions;
- Read(Pipe p, String verificationRoot, boolean noFollowLinks) {
+ Read(Pipe p, String verificationRoot, OpenOption... openOptions) {
this.p = p;
this.verificationRoot = verificationRoot;
- this.noFollowLinks = noFollowLinks;
+ this.openOptions = openOptions;
}
@Override
public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- try (InputStream fis = newInputStreamDenyingSymlinkAsNeeded(f, verificationRoot, noFollowLinks); OutputStream out = p.getOut()) {
+ try (InputStream fis = newInputStreamDenyingSymlinkAsNeeded(f, verificationRoot, openOptions); OutputStream out = p.getOut()) {
org.apache.commons.io.IOUtils.copy(fis, out);
… diff truncated
core/src/main/java/hudson/model/DirectoryBrowserSupport.java+37 12
@@ -34,6 +34,8 @@
import java.io.Serializable;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
@@ -49,6 +51,7 @@
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.ServletException;
@@ -83,6 +86,11 @@ public final class DirectoryBrowserSupport implements HttpResponse {
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
public static boolean ALLOW_SYMLINK_ESCAPE = SystemProperties.getBoolean(DirectoryBrowserSupport.class.getName() + ".allowSymlinkEscape");
+ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
+ public static boolean ALLOW_TMP_DISPLAY = SystemProperties.getBoolean(DirectoryBrowserSupport.class.getName() + ".allowTmpEscape");
+
+ private static final Pattern TMPDIR_PATTERN = Pattern.compile(".+@tmp/.*");
+
/**
* Escape hatch for the protection against SECURITY-2481. If enabled, the absolute paths on Windows will be allowed.
*/
@@ -264,7 +272,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
baseFile = root.child(base);
}
- if (baseFile.hasSymlink(getNoFollowLinks())) {
+ if (baseFile.hasSymlink(getOpenOptions()) || hasTmpDir(baseFile, base, getOpenOptions())) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
@@ -280,13 +288,13 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
includes = rest;
prefix = "";
}
- baseFile.zip(rsp.getOutputStream(), includes, null, true, getNoFollowLinks(), prefix);
+ baseFile.zip(rsp.getOutputStream(), includes, null, true, prefix, getOpenOptions());
return;
}
if (plain) {
rsp.setContentType("text/plain;charset=UTF-8");
try (OutputStream os = rsp.getOutputStream()) {
- for (VirtualFile kid : baseFile.list(getNoFollowLinks())) {
+ for (VirtualFile kid : baseFile.list(getOpenOptions())) {
os.write(kid.getName().getBytes(StandardCharsets.UTF_8));
if (kid.isDirectory()) {
os.write('/');
@@ -310,14 +318,16 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
List<List<Path>> glob = null;
boolean patternUsed = rest.length() > 0;
boolean containsSymlink = false;
- if (patternUsed) {
+ boolean containsTmpDir = false;
+ if (patternUsed) {
// the rest is Ant glob pattern
glob = patternScan(baseFile, rest, createBackRef(restSize));
} else
if (serveDirIndex) {
// serve directory index
glob = baseFile.run(new BuildChildPaths(root, baseFile, req.getLocale()));
- containsSymlink = baseFile.containsSymLinkChild(getNoFollowLinks());
+ containsSymlink = baseFile.containsSymLinkChild(getOpenOptions());
+ containsTmpDir = baseFile.containsTmpDirChild(getOpenOptions());
}
if (glob != null) {
@@ -333,6 +343,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
req.setAttribute("pattern", rest);
req.setAttribute("dir", baseFile);
req.setAttribute("showSymlinkWarning", containsSymlink);
+ req.setAttribute("showTmpDirWarning", containsTmpDir);
if (ResourceDomainConfiguration.isResourceRequest(req)) {
req.getView(this, "plaindir.jelly").forward(req, rsp);
} else {
@@ -378,7 +389,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
if (view) {
InputStream in;
try {
- in = baseFile.open(getNoFollowLinks());
+ in = baseFile.open(getOpenOptions());
} catch (IOException ioe) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -406,7 +417,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
}
InputStream in;
try {
- in = baseFile.open(getNoFollowLinks());
+ in = baseFile.open(getOpenOptions());
} catch (IOException ioe) {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -429,6 +440,13 @@ public Boolean call() throws IOException {
}
}
+ private boolean hasTmpDir(VirtualFile baseFile, String base, OpenOption[] openOptions) {
+ if (FilePath.isTmpDir(baseFile.getName(), openOptions)) {
+ return true;
+ }
+ return FilePath.isIgnoreTmpDirs(openOptions) && TMPDIR_PATTERN.matcher(base).matches();
+ }
+
private List<List<Path>> keepReadabilityOnlyOnDescendants(VirtualFile root, boolean patternUsed, List<List<Path>> pathFragmentsList) {
Stream<List<Path>> pathFragmentsStream = pathFragmentsList.stream().map((List<Path> pathFragments) -> {
List<Path> mappedFragments = new ArrayList<>(pathFragments.size());
@@ -754,7 +772,7 @@ private static final class BuildChildPaths extends MasterToSlaveCallable<List<Li
private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale) throws IOException {
List<List<Path>> r = new ArrayList<>();
- VirtualFile[] files = cur.list(getNoFollowLinks());
+ VirtualFile[] files = cur.list(getOpenOptions());
Arrays.sort(files, new FileComparator(locale));
for (VirtualFile f : files) {
@@ -769,7 +787,7 @@ private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale)
while (true) {
// files that don't start with '.' qualify for 'meaningful files', nor SCM related files
List<VirtualFile> sub = new ArrayList<>();
- for (VirtualFile vf : f.list(getNoFollowLinks())) {
+ for (VirtualFile vf : f.list(getOpenOptions())) {
String name = vf.getName();
if (!name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn")) {
sub.add(vf);
@@ -794,7 +812,7 @@ private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale)
* @param baseRef String like "../../../" that cancels the 'rest' portion. Can be "./"
*/
private static List<List<Path>> patternScan(VirtualFile baseDir, String pattern, String baseRef) throws IOException {
- Collection<String> files = baseDir.list(pattern, null, /* TODO what is the user expectation? */true, getNoFollowLinks());
+ Collection<String> files = baseDir.list(pattern, null, /* TODO what is the user expectation? */true, getOpenOptions());
if (!files.isEmpty()) {
List<List<Path>> r = new ArrayList<>(files.size());
@@ -837,8 +855,15 @@ private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, Lis
pathList.add(path);
}
- private static boolean getNoFollowLinks() {
- return !ALLOW_SYMLINK_ESCAPE;
+ private static OpenOption[] getOpenOptions() {
+ List<OpenOption> options = new ArrayList<>();
+ if (!ALLOW_SYMLINK_ESCAPE) {
+ options.add(LinkOption.NOFOLLOW_LINKS);
+ }
+ if (!ALLOW_TMP_DISPLAY) {
+ options.add(FilePath.DisplayOption.IGNORE_TMP_DIRS);
+ }
+ return options.toArray(new OpenOption[0]);
}
private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());
core/src/main/java/hudson/slaves/WorkspaceList.java+6 1
@@ -310,7 +310,7 @@ public void release() {
*/
@CheckForNull
public static FilePath tempDir(FilePath ws) {
- return ws.sibling(ws.getName() + COMBINATOR + "tmp");
+ return ws.sibling(ws.getName() + TMP_DIR_SUFFIX);
}
private static final Logger LOGGER = Logger.getLogger(WorkspaceList.class.getName());
@@ -320,4 +320,9 @@ public static FilePath tempDir(FilePath ws) {
* @since 2.244
*/
public static final String COMBINATOR = SystemProperties.getString(WorkspaceList.class.getName(), "@");
+
+ /**
+ * Suffix for temporary folders.
+ */
+ public static final String TMP_DIR_SUFFIX = COMBINATOR + "tmp";
}
core/src/main/java/hudson/util/DirScanner.java+7 6
@@ -8,6 +8,7 @@
import java.io.FileFilter;
import java.io.IOException;
import java.io.Serializable;
+import java.nio.file.OpenOption;
import java.util.HashSet;
import java.util.Set;
import org.apache.tools.ant.BuildException;
@@ -106,25 +107,25 @@ public static class Glob extends DirScanner {
private final String includes, excludes;
private boolean useDefaultExcludes = true;
- private final boolean followSymlinks;
+ private OpenOption[] openOptions;
public Glob(String includes, String excludes) {
- this(includes, excludes, true, true);
+ this(includes, excludes, true, new OpenOption[0]);
}
public Glob(String includes, String excludes, boolean useDefaultExcludes) {
- this(includes, excludes, useDefaultExcludes, true);
+ this(includes, excludes, useDefaultExcludes, new OpenOption[0]);
}
/**
* @since 2.275 and 2.263.2
*/
@Restricted(NoExternalUse.class)
- public Glob(String includes, String excludes, boolean useDefaultExcludes, boolean followSymlinks) {
+ public Glob(String includes, String excludes, boolean useDefaultExcludes, OpenOption... openOptions) {
this.includes = includes;
this.excludes = excludes;
this.useDefaultExcludes = useDefaultExcludes;
- this.followSymlinks = followSymlinks;
+ this.openOptions = openOptions;
}
@Override
@@ -136,7 +137,7 @@ public void scan(File dir, FileVisitor visitor) throws IOException {
}
FileSet fs = Util.createFileSet(dir, includes, excludes);
- fs.setFollowSymlinks(followSymlinks);
+ fs.setFollowSymlinks(!FilePath.isNoFollowLink(openOptions));
fs.setDefaultexcludes(useDefaultExcludes);
if (dir.exists()) {
core/src/main/java/hudson/util/io/ArchiverFactory.java+13 14
@@ -30,6 +30,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
+import java.nio.file.OpenOption;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -65,13 +66,14 @@ public abstract class ArchiverFactory implements Serializable {
public static ArchiverFactory ZIP = new ZipArchiverFactory();
/**
- * Zip format, without following symlinks.
+ * Zip format, with prefix and optional OpenOptions.
* @param prefix The portion of file path that will be added at the beginning of the relative path inside the archive.
* If non-empty, a trailing forward slash will be enforced.
+ * @param openOptions the options to apply when opening files.
*/
@Restricted(NoExternalUse.class)
- public static ArchiverFactory createZipWithoutSymlink(String prefix) {
- return new ZipWithoutSymLinksArchiverFactory(prefix);
+ public static ArchiverFactory createZipWithPrefix(String prefix, OpenOption... openOptions) {
+ return new ZipArchiverFactory(prefix, openOptions);
}
private static final class TarArchiverFactory extends ArchiverFactory {
@@ -91,26 +93,23 @@ public Archiver create(OutputStream out) throws IOException {
}
private static final class ZipArchiverFactory extends ArchiverFactory {
- @NonNull
- @Override
- public Archiver create(OutputStream out) {
- return new ZipArchiver(out);
- }
- private static final long serialVersionUID = 1L;
- }
-
- private static final class ZipWithoutSymLinksArchiverFactory extends ArchiverFactory {
private final String prefix;
+ private final OpenOption[] openOptions;
+
+ ZipArchiverFactory() {
+ this(null);
+ }
- ZipWithoutSymLinksArchiverFactory(String prefix) {
+ ZipArchiverFactory(String prefix, OpenOption... openOptions) {
this.prefix = prefix;
+ this.openOptions = openOptions;
}
@NonNull
@Override
public Archiver create(OutputStream out) {
- return new ZipArchiver(out, true, prefix);
+ return new ZipArchiver(out, prefix, openOptions);
}
private static final long serialVersionUID = 1L;
core/src/main/java/hudson/util/io/ZipArchiver.java+5 5
@@ -24,6 +24,7 @@
package hudson.util.io;
+import hudson.FilePath;
import hudson.Util;
import hudson.util.FileVisitor;
import hudson.util.IOUtils;
@@ -33,7 +34,6 @@
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
-import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.apache.commons.lang.StringUtils;
@@ -55,12 +55,13 @@ final class ZipArchiver extends Archiver {
private final String prefix;
ZipArchiver(OutputStream out) {
- this(out, false, "");
+ this(out, "");
}
// Restriction added for clarity, it's a package class, you should not use it outside of Jenkins core
@Restricted(NoExternalUse.class)
- ZipArchiver(OutputStream out, boolean failOnSymLink, String prefix) {
+ ZipArchiver(OutputStream out, String prefix, OpenOption... openOptions) {
+ this.openOptions = openOptions;
if (StringUtils.isBlank(prefix)) {
this.prefix = "";
} else {
@@ -68,7 +69,6 @@ final class ZipArchiver extends Archiver {
}
zip = new ZipOutputStream(out);
- openOptions = failOnSymLink ? new LinkOption[]{LinkOption.NOFOLLOW_LINKS} : new OpenOption[0];
zip.setEncoding(System.getProperty("file.encoding"));
zip.setUseZip64(Zip64Mode.AsNeeded);
}
@@ -97,7 +97,7 @@ public void visit(final File f, final String _relativePath) throws IOException {
fileZipEntry.setTime(basicFileAttributes.lastModifiedTime().toMillis());
fileZipEntry.setSize(basicFileAttributes.size());
zip.putNextEntry(fileZipEntry);
- try (InputStream in = Files.newInputStream(f.toPath(), openOptions)) {
+ try (InputStream in = FilePath.openInputStream(f, openOptions)) {
int len;
while ((len = in.read(buf)) >= 0)
zip.write(buf, 0, len);
core/src/main/java/jenkins/util/VirtualFile.java+64 52
@@ -48,6 +48,7 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
+import java.nio.file.OpenOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -185,12 +186,12 @@ public abstract class VirtualFile implements Comparable<VirtualFile>, Serializab
* Some VirtualFile subclasses may not be able to provide
* an implementation in which NOFOLLOW_LINKS is used or makes sense. Implementations are free
* to ignore openOptions. Some subclasses of VirtualFile may not have a concept of symlinks.
- * @param noFollowLinks if true then do not follow links.
+ * @param openOptions the options to apply when opening.
* @return a list of children (files and subdirectories); empty for a file or nonexistent directory
* @throws IOException if it could not be opened
*/
@Restricted(NoExternalUse.class)
- public @NonNull VirtualFile[] list(boolean noFollowLinks) throws IOException {
+ public @NonNull VirtualFile[] list(OpenOption... openOptions) throws IOException {
return list();
}
@@ -206,13 +207,13 @@ public boolean supportsQuickRecursiveListing() {
* only the file itself is considered.
*
* This base implementation ignores the existence of symlinks.
- * @param noFollowLinks if true, then do not follow links.
- * @return True if the file is a symlink or is referenced within a containing symlink
+ * @param openOptions the various open options to apply to the operation.
+ * @return True if the file is a symlink or is referenced within a containing symlink.
* directory before reaching the root directory.
* @throws IOException If there is a problem accessing the file.
*/
@Restricted(NoExternalUse.class)
- public boolean hasSymlink(boolean noFollowLinks) throws IOException {
+ public boolean hasSymlink(OpenOption... openOptions) throws IOException {
return false;
}
@@ -255,8 +256,9 @@ public boolean hasSymlink(boolean noFollowLinks) throws IOException {
* @throws IOException if this is not a directory, or listing was not possible for some other reason
* @since 2.118
*/
+ @Restricted(NoExternalUse.class)
public @NonNull Collection<String> list(@NonNull String includes, @CheckForNull String excludes, boolean useDefaultExcludes) throws IOException {
- return list(includes, excludes, useDefaultExcludes, false);
+ return list(includes, excludes, useDefaultExcludes, new OpenOption[0]);
}
/**
@@ -266,20 +268,20 @@ public boolean hasSymlink(boolean noFollowLinks) throws IOException {
* Implementations may wish to override this more efficiently.
* This method allows the user to specify that symlinks should not be followed by passing
- * noFollowLinks as true. However, some implementations may not be able to reliably
+ * LinkOption.NOFOLLOW_LINKS as true. However, some implementations may not be able to reliably
* prevent link following. The base implementation here in VirtualFile ignores this parameter.
* @param includes comma-separated Ant-style globs as per {@link Util#createFileSet(File, String, String)} using {@code /} as a path separator;
* the empty string means <em>no matches</em> (use {@link SelectorUtils#DEEP_TREE_MATCH} if you want to match everything except some excludes)
* @param excludes optional excludes in similar format to {@code includes}
* @param useDefaultExcludes as per {@link AbstractFileSet#setDefaultexcludes}
- * @param noFollowLinks if true then do not follow links.
+ * @param openOptions the options to apply when opening.
* @return a list of {@code /}-separated relative names of children (files directly inside or in subdirectories)
* @throws IOException if this is not a directory, or listing was not possible for some other reason
* @since 2.275 and 2.263.2
*/
@Restricted(NoExternalUse.class)
public @NonNull Collection<String> list(@NonNull String includes, @CheckForNull String excludes, boolean useDefaultExcludes,
- boolean noFollowLinks) throws IOException {
+ OpenOption... openOptions) throws IOException {
Collection<String> r = run(new CollectFiles(this));
List<TokenizedPattern> includePatterns = patterns(includes);
List<TokenizedPattern> excludePatterns = patterns(excludes);
@@ -295,7 +297,17 @@ public boolean hasSymlink(boolean noFollowLinks) throws IOException {
}
@Restricted(NoExternalUse.class)
- public boolean containsSymLinkChild(boolean noFollowLinks) throws IOException {
+ public boolean containsSymLinkChild(OpenOption... openOptions) throws IOException {
+ return false;
+ }
+
+ @Restricted(NoExternalUse.class)
+ public boolean containsTmpDirChild(OpenOption... openOptions) throws IOException {
+ for (VirtualFile child : list()) {
+ if (child.isDirectory() && FilePath.isTmpDir(child.getName(), openOptions)) {
+ return true;
+ }
+ }
return false;
}
@@ -347,15 +359,15 @@ private List<TokenizedPattern> patterns(String patts) {
* the empty string means <em>no matches</em> (use {@link SelectorUtils#DEEP_TREE_MATCH} if you want to match everything except some excludes)
* @param excludes optional excludes in similar format to {@code includes}
* @param useDefaultExcludes as per {@link AbstractFileSet#setDefaultexcludes}
- * @param noFollowLinks if true then do not follow links.
* @param prefix the partial path that will be added before each entry inside the archive.
* If non-empty, a trailing slash will be enforced.
+ * @param openOptions the options to apply when opening.
* @return the number of files inside the archive (not the folders)
* @throws IOException if this is not a directory, or listing was not possible for some other reason
* @since 2.275 and 2.263.2
*/
public int zip(OutputStream outputStream, String includes, String excludes, boolean useDefaultExcludes,
- boolean noFollowLinks, String prefix) throws IOException {
+ String prefix, OpenOption... openOptions) throws IOException {
String correctPrefix;
if (StringUtils.isBlank(prefix)) {
correctPrefix = "";
@@ -363,19 +375,19 @@ public int zip(OutputStream outputStream, String includes, String excludes, bool
correctPrefix = Util.ensureEndsWith(prefix, "/");
}
- Collection<String> files = list(includes, excludes, useDefaultExcludes, noFollowLinks);
+ Collection<String> files = list(includes, excludes, useDefaultExcludes, openOptions);
try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
zos.setEncoding(System.getProperty("file.encoding")); // TODO JENKINS-20663 make this overridable via query parameter
for (String relativePath : files) {
VirtualFile virtualFile = this.child(relativePath);
- sendOneZipEntry(zos, virtualFile, relativePath, noFollowLinks, correctPrefix);
+ sendOneZipEntry(zos, virtualFile, relativePath, correctPrefix, openOptions);
}
}
return files.size();
}
- private void sendOneZipEntry(ZipOutputStream zos, VirtualFile vf, String relativePath, boolean noFollowLinks, String prefix) throws IOException {
+ private void sendOneZipEntry(ZipOutputStream zos, VirtualFile vf, String relativePath, String prefix, OpenOption... openOptions) throws IOException {
// In ZIP archives "All slashes MUST be forward slashes" (http://pkware.com/documents/casestudies/APPNOTE.TXT)
// TODO On Linux file names can contain backslashes which should not treated as file separators.
// Unfortunately, only the file separator char of the master is known (File.separatorChar)
@@ -386,7 +398,7 @@ private void sendOneZipEntry(ZipOutputStream zos, VirtualFile vf, String relativ
e.setTime(vf.lastModified());
zos.putNextEntry(e);
- try (InputStream in = vf.open(noFollowLinks)) {
+ try (InputStream in = vf.open(openOptions)) {
// hudson.util.IOUtils is already present
org.apache.commons.io.IOUtils.copy(in, zos);
}
@@ -444,12 +456,12 @@ public int mode() throws IOException {
/**
* Opens an input stream on the file so its contents can be read.
*
- * @param noFollowLinks if true do not follow links.
+ * @param openOptions the options to apply when opening.
* @return an open stream
* @throws IOException if it could not be opened
*/
@Restricted(NoExternalUse.class)
- public InputStream open(boolean noFollowLinks) throws IOException {
+ public InputStream open(OpenOption... openOptions) throws IOException {
return open();
}
@@ -623,12 +635,12 @@ private static final class FileVF extends VirtualFile {
@NonNull
@Override
- public VirtualFile[] list(boolean noFollowLinks) throws IOException {
+ public VirtualFile[] list(OpenOption... openOptions) throws IOException {
String rootPath = determineRootPath();
File[] kids = f.listFiles();
List<VirtualFile> contents = new ArrayList<>(kids.length);
for (File child : kids) {
- if (!FilePath.isSymlink(child, rootPath, noFollowLinks)) {
+ if (!FilePath.isSymlink(child, rootPath, openOptions) && !FilePath.isTmpDir(child, rootPath, openOptions)) {
contents.add(new FileVF(child, root));
}
}
@@ -668,27 +680,27 @@ public Collection<String> list(String includes, String excludes, boolean useDefa
@Override
public Collection<String> list(String includes, String excludes, boolean useDefaultExcludes,
- boolean noFollowLinks) throws IOException {
+ OpenOption... openOptions) throws IOException {
String rootPath = determineRootPath();
- return new Scanner(includes, excludes, useDefaultExcludes, rootPath, noFollowLinks).invoke(f, null);
+ return new Scanner(includes, excludes, useDefaultExcludes, rootPath, openOptions).invoke(f, null);
}
@Override
public int zip(OutputStream outputStream, String includes, String excludes, boolean useDefaultExcludes,
- boolean noFollowLinks, String prefix) throws IOException {
+ String prefix, OpenOption... openOptions) throws IOException {
String rootPath = determineRootPath();
- DirScanner.Glob globScanner = new DirScanner.Glob(includes, excludes, useDefaultExcludes, !noFollowLinks);
- ArchiverFactory archiverFactory = noFollowLinks ? ArchiverFactory.createZipWithoutSymlink(prefix) : ArchiverFactory.ZIP;
+ DirScanner.Glob globScanner = new DirScanner.Glob(includes, excludes, useDefaultExcludes, openOptions);
+ ArchiverFactory archiverFactory = prefix == null ? ArchiverFactory.ZIP : ArchiverFactory.createZipWithPrefix(prefix, openOptions);
try (Archiver archiver = archiverFactory.create(outputStream)) {
- globScanner.scan(f, FilePath.ignoringSymlinks(archiver, rootPath, noFollowLinks));
+ globScanner.scan(f, FilePath.ignoringTmpDirs(FilePath.ignoringSymlinks(archiver, rootPath, openOptions), rootPath, openOptions));
return archiver.countEntries();
}
}
@Override
- public boolean hasSymlink(boolean noFollowLinks) throws IOException {
+ public boolean hasSymlink(OpenOption... openOptions) throws IOException {
String rootPath = determineRootPath();
- return FilePath.isSymlink(f, rootPath, noFollowLinks);
+ return FilePath.isSymlink(f, rootPath, openOptions);
}
@Override public VirtualFile child(String name) {
@@ -735,18 +747,18 @@ public boolean hasSymlink(boolean noFollowLinks) throws IOException {
}
@Override
- public InputStream open(boolean noFollowLinks) throws IOException {
+ public InputStream open(OpenOption... openOptions) throws IOException {
String rootPath = determineRootPath();
- InputStream inputStream = FilePath.newInputStreamDenyingSymlinkAsNeeded(f, rootPath, noFollowLinks);
+ InputStream inputStream = FilePath.newInputStreamDenyingSymlinkAsNeeded(f, rootPath, openOptions);
return inputStream;
}
@Override
- public boolean containsSymLinkChild(boolean noFollowLinks) {
+ public boolean containsSymLinkChild(OpenOption... openOptions) {
String rootPath = determineRootPath();
File[] kids = f.listFiles();
for (File child : kids) {
- if (FilePath.isSymlink(child, rootPath, noFollowLinks)) {
+ if (FilePath.isSymlink(child, rootPath, openOptions)) {
return true;
}
}
@@ -926,9 +938,9 @@ private VirtualFile[] convertChildrenToVirtualFile(List<FilePath> kids) {
@NonNull
@Override
- public VirtualFile[] list(boolean noFollowLinks) throws IOException {
+ public VirtualFile[] list(OpenOption... openOptions) throws IOException {
try {
- List<FilePath> kids = f.list(root, noFollowLinks);
+ List<FilePath> kids = f.list(root, openOptions);
return convertChildrenToVirtualFile(kids);
} catch (InterruptedException x) {
throw new IOException(x);
@@ -936,24 +948,24 @@ public VirtualFile[] list(boolean noFollowLinks) throws IOException {
}
@Override
- public boolean containsSymLinkChild(boolean noFollowLinks) throws IOException {
+ public boolean containsSymLinkChild(OpenOption... openOptions) throws IOException {
try {
- return f.containsSymlink(root, noFollowLinks);
+ return f.containsSymlink(root, openOptions);
} catch (InterruptedException x) {
throw new IOException(x);
}
}
@Override
- public boolean hasSymlink(boolean noFollowLinks) throws IOException {
+ public boolean hasSymlink(OpenOption... openOptions) throws IOException {
try {
- return f.hasSymlink(root, noFollowLinks);
+ return f.hasSymlink(root, openOptions);
} catch (InterruptedException x) {
throw new IOException(x);
}
}
- @Override public boolean supportsQuickRecursiveListing() {
+ @Override public boolean supportsQuickRecursiveListing() {
return this.f.getChannel() == FilePath.localChannel;
}
@@ -989,10 +1001,10 @@ public boolean hasSymlink(boolean noFollowLinks) throws IOException {
@Override
public Collection<String> list(String includes, String excludes, boolean useDefaultExcludes,
- boolean noFollowLinks) throws IOException {
+ OpenOption... openOptions) throws IOException {
try {
String rootPath = root == null ? null : root.getRemote();
- return f.act(new Scanner(includes, excludes, useDefaultExcludes, rootPath, noFollowLinks));
+ return f.act(new Scanner(includes, excludes, useDefaultExcludes, rootPath, openOptions));
} catch (InterruptedException x) {
throw new IOException(x);
}
@@ -1000,11 +1012,11 @@ public Collection<String> list(String includes, String excludes, boolean useDefa
@Override
public int zip(OutputStream outputStream, String includes, String excludes, boolean useDefaultExcludes,
- boolean noFollowLinks, String prefix) throws IOException {
+ String prefix, OpenOption... openOptions) throws IOException {
try {
String rootPath = root == null ? null : root.getRemote();
- DirScanner.Glob globScanner = new DirScanner.Glob(includes, excludes, useDefaultExcludes, !noFollowLinks);
- return f.zip(outputStream, globScanner, rootPath, noFollowLinks, prefix);
+ DirScanner.Glob globScanner = new DirScanner.Glob(includes, excludes, useDefaultExcludes, openOptions);
+ return f.zip(outputStream, globScanner, rootPath, prefix, openOptions);
} catch (InterruptedException x) {
throw new IOException(x);
}
@@ -1054,9 +1066,9 @@ public int zip(OutputStream outputStream, String includes, String excludes, bool
}
}
- @Override public InputStream open(boolean noFollowLinks) throws IOException {
+ @Override public InputStream open(OpenOption... openOptions) throws IOException {
try {
- return f.read(root, noFollowLinks);
+ return f.read(root, openOptions);
} catch (InterruptedException x) {
throw new IOException(x);
}
@@ -1144,18 +1156,18 @@ private static final class Scanner extends MasterToSlaveFileCallable<List<String
private final String includes, excludes;
private final boolean useDefaultExcludes;
private final String verificationRoot;
- private final boolean noFollowLinks;
+ private OpenOption[] openOptions;
- Scanner(String includes, String excludes, boolean useDefaultExcludes, String verificationRoot, boolean noFollowLinks) {
+ Scanner(String includes, String excludes, boolean useDefaultExcludes, String verificationRoot, OpenOption... openOptions) {
this.includes = includes;
this.excludes = excludes;
this.useDefaultExcludes = useDefaultExcludes;
this.verificationRoot = verificationRoot;
- this.noFollowLinks = noFollowLinks;
+ this.openOptions = openOptions;
}
Scanner(String includes, String excludes, boolean useDefaultExcludes) {
- this(includes, excludes, useDefaultExcludes, null, false);
+ this(includes, excludes, useDefaultExcludes, null, new OpenOption[0]);
}
@Override public List<String> invoke(File f, VirtualChannel channel) throws IOException {
@@ -1169,8 +1181,8 @@ public void visit(File f, String relativePath) {
paths.add(relativePath.replace('\\', '/'));
}
};
- DirScanner.Glob globScanner = new DirScanner.Glob(includes, excludes, useDefaultExcludes, !noFollowLinks);
- globScanner.scan(f, FilePath.ignoringSymlinks(listing, verificationRoot, noFollowLinks));
+ DirScanner.Glob globScanner = new DirScanner.Glob(includes, excludes, useDefaultExcludes, openOptions);
+ globScanner.scan(f, FilePath.ignoringTmpDirs(FilePath.ignoringSymlinks(listing, verificationRoot, openOptions), verificationRoot, openOptions));
return paths;
}
core/src/main/resources/hudson/model/DirectoryBrowserSupport/dir.jelly+12 0
@@ -54,6 +54,12 @@ THE SOFTWARE.
${%Symlinks are hidden}
</p>
</j:if>
+ <j:if test="${showTmpDirWarning}">
+ <p>
+ <img id="tmpdiralert" src="${imagesURL}/16x16/warning.png"/>
+ ${%Tmp directories are hidden}
+ </p>
+ </j:if>
</j:when>
<j:otherwise>
<table class="fileList">
@@ -122,6 +128,12 @@ THE SOFTWARE.
${%Symlinks are hidden}
</p>
</j:if>
+ <j:if test="${showTmpDirWarning}">
+ <p>
+ <img id="alert" src="${imagesURL}/16x16/warning.png"/>
+ ${%Tmp directories are hidden}
+ </p>
+ </j:if>
</j:otherwise>
</j:choose>
</div>
More files changed — see the full commit.

References