diff --git a/.gitignore b/.gitignore index 60ba89065c90d09f0cc1ea8935b0bf08b691b0e9..5c8b1b7ac8b0be6f49db372e82de07e26bdac765 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ out .project build +# VSCode workspace file +*.code-workspace + # vim *~ *.swp @@ -49,4 +52,5 @@ jenkins_*.changes *.zip push-build.sh war/node_modules/ +war/yarn-error.log .java-version diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 0000000000000000000000000000000000000000..a2d496cc2b211139d6f9b2eee27cb0be380ec8f5 --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,7 @@ + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.0-beta-4 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000000000000000000000000000000000000..e54fdacfe62468e4e85ebe51890384926933b476 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1 @@ +-Pmight-produce-incrementals diff --git a/BUILDING.TXT b/BUILDING.TXT deleted file mode 100644 index bde5cadf353210259628baa34b1813f0acddc5b1..0000000000000000000000000000000000000000 --- a/BUILDING.TXT +++ /dev/null @@ -1,11 +0,0 @@ -If you want simply to have the jenkins.war as fast as possible (without test -execution), run: - - mvn clean install -pl war -am -DskipTests - -The WAR file will be in war/target/jenkins.war (you can play with it) - -For more information on building Jenkins, visit -https://wiki.jenkins-ci.org/display/JENKINS/Building+Jenkins - -Have Fun !! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f99fe77067ef6c25597bf54df9e0debdaff27fef..4311a35ac21abf137bfc69b9415f3f62bb49f8d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,131 @@ # Contributing to Jenkins -For information on contributing to Jenkins, check out https://jenkins.io/redirect/contribute/. That page will help you get started with contributing to Jenkins. +This page provides information about contributing code to the Jenkins core codebase. + +:exclamation: There's a lot more to the Jenkins project than just code. For information on contributing to the Jenkins project overall, check out [Participate]. + +## Getting started + +1. Fork the repository on GitHub +2. Clone the forked repository to your machine +3. Install the development tools. In order to develop Jenkins, you need the following tools: + * Java Development Kit (JDK) 8. + - In Jenkins project we usually use [OpenJDK], + but you can use other JDKs as well. + - Java 9+ is **not supported** in Jenkins. + * Maven 3.5.3 or above. You can [download maven]. + * Any IDE which supports importing Maven projects. +4. Setup your development environment as described in [Preparing for Plugin Development] + +If you want to contribute to Jenkins or just learn about the project, +you can start by fixing some easier issues. +In the Jenkins issue tracker we mark such issues as `newbie-friendly`. +You can find them +using this query for [newbie friendly issues]. + +## Building and Debugging + +The build flow for Jenkins core is built around Maven. +There is a description of the [building and debugging process]. + +If you want simply to have the `jenkins.war` file as fast as possible without tests, run: + + mvn clean package -pl war -am -DskipTests -Dfindbugs.skip + +The WAR file will be created in `war/target/jenkins.war`. +After that you can start Jenkins using Java CLI ([guide]). +If you want to debug this WAR file without using Maven plugins, +You can just start the executable with [Remote Debug Flags] +and then attach IDE Debugger to it. + +## Testing changes + +Jenkins core includes unit and functional tests as a part of the repository. + +Functional tests (`test` module) take a while even on server-grade machines. +Most of the tests will be launched by the continuous integration instance, +so there is no strict need to run full test suites before proposing a pull request. + +There are 3 profiles for tests: + +* `light-test` - only unit tests, no functional tests +* `smoke-test` - run unit tests + a number of functional tests +* `all-tests` - Runs all tests, with re-run (default) + +In addition to the included tests, you can also find extra integration and UI +tests in the [Acceptance Test Harness (ATH)] repository. +If you propose complex UI changes, you should create new ATH tests for them. + +## Proposing Changes + +The Jenkins project source code repositories are hosted at GitHub. +All proposed changes are submitted and code reviewed using the _GitHub Pull Request_ process. + +To submit a pull request: + +1. Commit changes and push them to your fork on GitHub. +It is a good practice is to create branches instead of pushing to master. +2. In GitHub Web UI click the _New Pull Request_ button +3. Select `jenkinsci` as _base fork_ and `master` as `base`, then click _Create Pull Request_ + * We integrate all changes into the master branch towards the Weekly releases + * After that the changes may be backported to the current LTS baseline by the LTS Team. + Read more about the [backporting process] +4. Fill in the Pull Request description according to the [proposed template]. +5. Click _Create Pull Request_ +6. Wait for CI results/reviews, process the feedback. + * If you do not get feedback after 3 days, feel free to ping `@jenkinsci/code-reviewers` to CC. + * Usually we merge pull requests after 2 votes from reviewers or after 1 vote and 1-week delay without extra reviews. + +Once your Pull Request is ready to be merged, +the repository maintainers will integrate it, prepare changelogs and +ensure it gets released in one of incoming Weekly releases. +There is no additional action required from pull request authors at this point. + +## Copyright + +Jenkins core is licensed under [MIT license], with a few exceptions in bundled classes. +We consider all contributions as MIT unless it's explicitly stated otherwise. +MIT-incompatible code contributions will be rejected. +Contributions under MIT-compatible licenses may be also rejected if they are not ultimately necessary. + +We **Do NOT** require pull request submitters to sign the [contributor agreement] +as long as the code is licensed under MIT and merged by one of the contributors with the signed agreement. + +We still encourage people to sign the contributor agreement if they intend to submit more than a few pull requests. +Signing is also a mandatory prerequisite for getting merge/push permissions to core repositories +and for joining teams like [Jenkins Security Team]. + +## Continuous Integration + +The Jenkins project has a Continuous Integration server... powered by Jenkins, of course. +It is located at [ci.jenkins.io]. + +The Jenkins project uses [Jenkins Pipeline] to run builds. +The code for the core build flow is stored in the [Jenkinsfile] in the repository root. +If you want to update that build flow (e.g. "add more checks"), +just submit a pull request. + +# Links + +* [Jenkins Contribution Landing Page](https://jenkins.io/participate/) +* [Jenkins IRC Channel](https://jenkins.io/chat/) +* [Beginners Guide To Contributing](https://wiki.jenkins.io/display/JENKINS/Beginners+Guide+to+Contributing) +* [List of newbie-friendly issues in the core](https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open%2C%20%22In%20Progress%22%2C%20Reopened)%20AND%20component%20%3D%20core%20AND%20labels%20in%20(newbie-friendly)) + +[download maven]: https://maven.apache.org/download.cgi +[Preparing for Plugin Development]: https://jenkins.io/doc/developer/tutorial/prepare/ +[newbie friendly issues]: https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open%2C%20%22In%20Progress%22%2C%20Reopened)%20AND%20component%20%3D%20core%20AND%20labels%20in%20(newbie-friendly) +[OpenJDK]: http://openjdk.java.net/ +[Participate]: https://jenkins.io/participate/ +[building and debugging process]: https://jenkins.io/doc/developer/building/ +[guide]: https://wiki.jenkins.io/display/JENKINS/Starting+and+Accessing+Jenkins +[Remote Debug Flags]: https://stackoverflow.com/questions/975271/remote-debugging-a-java-application +[Acceptance Test Harness (ATH)]: https://github.com/jenkinsci/acceptance-test-harness +[backporting process]: https://jenkins.io/download/lts/ +[proposed template]: .github/PULL_REQUEST_TEMPLATE.md +[MIT license]: ./LICENSE.txt +[contributor agreement]: https://wiki.jenkins.io/display/JENKINS/Copyright+on+source+code +[Jenkins Security Team]: https://jenkins.io/security/#team +[ci.jenkins.io]: https://ci.jenkins.io/ +[Jenkins Pipeline]: https://jenkins.io/doc/book/pipeline/ +[Jenkinsfile]: ./Jenkinsfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d4541bdfcbaa7904cfc9264f6a823d80d1b55992 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# This is a Dockerfile definition for Experimental Docker builds. +# DockerHub: https://hub.docker.com/r/jenkins/jenkins-experimental/ +# If you are looking for official images, see https://github.com/jenkinsci/docker +FROM maven:3.5.4-jdk-8 as builder + +COPY .mvn/ /jenkins/src/.mvn/ +COPY cli/ /jenkins/src/cli/ +COPY core/ /jenkins/src/core/ +COPY src/ /jenkins/src/src/ +COPY test/ /jenkins/src/test/ +COPY war/ /jenkins/src/war/ +COPY *.xml /jenkins/src/ +COPY LICENSE.txt /jenkins/src/LICENSE.txt +COPY licenseCompleter.groovy /jenkins/src/licenseCompleter.groovy +COPY show-pom-version.rb /jenkins/src/show-pom-version.rb + +WORKDIR /jenkins/src/ +RUN mvn clean install --batch-mode -Plight-test + +# The image is based on the previous weekly, new changes in jenkinci/docker are not applied +FROM jenkins/jenkins:latest + +LABEL Description="This is an experimental image for the master branch of the Jenkins core" Vendor="Jenkins Project" + +COPY --from=builder /jenkins/src/war/target/jenkins.war /usr/share/jenkins/jenkins.war +ENTRYPOINT ["tini", "--", "/usr/local/bin/jenkins.sh"] diff --git a/Jenkinsfile b/Jenkinsfile index 70bd8d87dfaddb5d075c7ae40b07fab42aacf812..3cecdfcbb7b16bac414c800ad0163e37e80dbd5e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,15 +13,18 @@ def runTests = true def failFast = false -properties([buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '20'))]) +properties([buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '20')), durabilityHint('PERFORMANCE_OPTIMIZED')]) // see https://github.com/jenkins-infra/documentation/blob/master/ci.adoc for information on what node types are available def buildTypes = ['Linux', 'Windows'] +def jdks = [8, 11] def builds = [:] for(i = 0; i < buildTypes.size(); i++) { +for(j = 0; j < jdks.size(); j++) { def buildType = buildTypes[i] - builds[buildType] = { + def jdk = jdks[j] + builds["${buildType}-jdk${jdk}"] = { node(buildType.toLowerCase()) { timestamps { // First stage is actually checking out the source. Since we're using Multibranch @@ -30,55 +33,89 @@ for(i = 0; i < buildTypes.size(); i++) { checkout scm } + def changelistF = "${pwd tmp: true}/changelist" + def m2repo = "${pwd tmp: true}/m2repo" + // Now run the actual build. stage("${buildType} Build / Test") { timeout(time: 180, unit: 'MINUTES') { // See below for what this method does - we're passing an arbitrary environment // variable to it so that JAVA_OPTS and MAVEN_OPTS are set correctly. withMavenEnv(["JAVA_OPTS=-Xmx1536m -Xms512m", - "MAVEN_OPTS=-Xmx1536m -Xms512m"]) { + "MAVEN_OPTS=-Xmx1536m -Xms512m"], jdk) { // Actually run Maven! // -Dmaven.repo.local=… tells Maven to create a subdir in the temporary directory for the local Maven repository - def mvnCmd = "mvn -Pdebug -U clean install javadoc:javadoc ${runTests ? '-Dmaven.test.failure.ignore' : '-DskipTests'} -V -B -Dmaven.repo.local=${pwd tmp: true}/m2repo -s settings-azure.xml -e" + def mvnCmd = "mvn -Pdebug -U -Dset.changelist help:evaluate -Dexpression=changelist -Doutput=$changelistF clean install ${runTests ? '-Dmaven.test.failure.ignore' : '-DskipTests'} -V -B -Dmaven.repo.local=$m2repo -s settings-azure.xml -e" + if(isUnix()) { sh mvnCmd - sh 'test `git status --short | tee /dev/stderr | wc --bytes` -eq 0' + sh 'git add . && git diff --exit-code HEAD' } else { - bat "$mvnCmd -Duser.name=yay" // INFRA-1032 workaround + bat mvnCmd } } } } // Once we've built, archive the artifacts and the test results. - stage("${buildType} Archive Artifacts / Test Results") { - def files = findFiles(glob: '**/target/*.jar, **/target/*.war, **/target/*.hpi') - renameFiles(files, buildType.toLowerCase()) - - archiveArtifacts artifacts: '**/target/*.jar, **/target/*.war, **/target/*.hpi', - fingerprint: true + stage("${buildType} Publishing") { if (runTests) { - junit healthScaleFactor: 20.0, testResults: '**/target/surefire-reports/*.xml' + junit healthScaleFactor: 20.0, testResults: '*/target/surefire-reports/*.xml' + archiveArtifacts allowEmptyArchive: true, artifacts: '**/target/surefire-reports/*.dumpstream' + } + if (buildType == 'Linux') { + def changelist = readFile(changelistF) + dir(m2repo) { + archiveArtifacts artifacts: "**/*$changelist/*$changelist*", + excludes: '**/*.lastUpdated,**/jenkins-test*/', + allowEmptyArchive: true, // in case we forgot to reincrementalify + fingerprint: true + } } } } } } +}} + +// TODO: ATH flow now supports Java 8 only, it needs to be reworked (INFRA-1690) +builds.ath = { + node("docker&&highmem") { + // Just to be safe + deleteDir() + def fileUri + def metadataPath + dir("sources") { + checkout scm + withMavenEnv(["JAVA_OPTS=-Xmx1536m -Xms512m", + "MAVEN_OPTS=-Xmx1536m -Xms512m"], 8) { + sh "mvn --batch-mode --show-version -DskipTests -am -pl war package -Dmaven.repo.local=${pwd tmp: true}/m2repo -s settings-azure.xml" + } + dir("war/target") { + fileUri = "file://" + pwd() + "/jenkins.war" + } + metadataPath = pwd() + "/essentials.yml" + } + dir("ath") { + runATH jenkins: fileUri, metadataFile: metadataPath + } + } } builds.failFast = failFast parallel builds +infra.maybePublishIncrementals() // This method sets up the Maven and JDK tools, puts them in the environment along // with whatever other arbitrary environment variables we passed in, and runs the // body we passed in within that environment. -void withMavenEnv(List envVars = [], def body) { +void withMavenEnv(List envVars = [], def javaVersion, def body) { // The names here are currently hardcoded for my test environment. This needs // to be made more flexible. // Using the "tool" Workflow call automatically installs those tools on the // node. String mvntool = tool name: "mvn", type: 'hudson.tasks.Maven$MavenInstallation' - String jdktool = tool name: "jdk8", type: 'hudson.model.JDK' + String jdktool = tool name: "jdk${javaVersion}", type: 'hudson.model.JDK' // Set JAVA_HOME, MAVEN_HOME and special PATH variables for the tools we're // using. @@ -92,15 +129,3 @@ void withMavenEnv(List envVars = [], def body) { body.call() } } - -void renameFiles(def files, String prefix) { - for(i = 0; i < files.length; i++) { - def newPath = files[i].path.replace(files[i].name, "${prefix}-${files[i].name}") - def rename = "${files[i].path} ${newPath}" - if(isUnix()) { - sh "mv ${rename}" - } else { - bat "move ${rename}" - } - } -} diff --git a/README.md b/README.md index 6dc6c47ecef715c771fcfdeb9ccd83cc45cf4962..7ef6c08a8ec40120d755931ce679e5ca870c5bda 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Non-source downloads such as WAR files and several Linux packages can be found o Our latest and greatest source of Jenkins can be found on [GitHub]. Fork us! # Contributing to Jenkins -Follow [contributing](CONTRIBUTING.md) file. +Follow the [contributing](CONTRIBUTING.md) file. # News and Website All information about Jenkins can be found on our [website]. Follow us on Twitter [@jenkinsci]. diff --git a/cli/pom.xml b/cli/pom.xml index 68923da01ad98509e5ff42c8e2d5e926886f9181..0a5aa6a5c328e6072e7a164f4bf974656b676cb0 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -4,8 +4,8 @@ org.jenkins-ci.main - pom - 2.68-SNAPSHOT + jenkins-parent + ${revision}${changelist} cli @@ -13,6 +13,11 @@ Jenkins cli Command line interface for Jenkins + + + Medium + + org.powermock @@ -21,17 +26,20 @@ org.powermock - powermock-api-mockito + powermock-api-mockito2 test org.kohsuke access-modifier-annotation + + org.jenkins-ci + annotation-indexer + commons-codec commons-codec - 1.4 commons-io @@ -55,9 +63,15 @@ org.apache.sshd sshd-core - 1.2.0 + 1.7.0 true + + + net.i2p.crypto + eddsa + 0.3.0 + org.slf4j slf4j-jdk14 @@ -68,6 +82,15 @@ trilead-ssh2 build214-jenkins-1 + + com.google.code.findbugs + annotations + provided + + + commons-lang + commons-lang + @@ -78,17 +101,19 @@ - attached + single package - jar-with-dependencies + + jar-with-dependencies + hudson.cli.CLI - ${build.version} + ${project.version} diff --git a/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties b/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties index 39b91895a20a85254399d1f4f0206c6af45ff094..defbd48204e4784090fe0e17e28fe5bc395abf47 100644 --- a/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties +++ b/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties @@ -1 +1 @@ -version=${build.version} +version=${project.version} diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index c9d7149ca3d4cb9cb4bcf5b4520254080949d661..0ffef53f9305a6e9d260826e092e805fe9b8465e 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -77,6 +77,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.*; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; /** * CLI entry point to Jenkins. @@ -306,9 +307,7 @@ public class CLI implements AutoCloseable { flushURLConnection(head); if (p1==null && p2==null) { - // we aren't finding headers we are expecting. Is this even running Jenkins? - if (head.getHeaderField("X-Hudson")==null && head.getHeaderField("X-Jenkins")==null) - throw new IOException("There's no Jenkins running at "+url); + verifyJenkinsConnection(head); throw new IOException("No X-Jenkins-CLI2-Port among " + head.getHeaderFields().keySet()); } @@ -317,6 +316,27 @@ public class CLI implements AutoCloseable { else return new CliPort(new InetSocketAddress(h,Integer.parseInt(p1)),identity,1); } + /** + * Make sure the connection is open against Jenkins server. + * + * @param c The open connection. + * @throws IOException in case of communication problem. + * @throws NotTalkingToJenkinsException when connection is not made to Jenkins service. + */ + /*package*/ static void verifyJenkinsConnection(URLConnection c) throws IOException { + if (c.getHeaderField("X-Hudson")==null && c.getHeaderField("X-Jenkins")==null) + throw new NotTalkingToJenkinsException(c); + } + /*package*/ static final class NotTalkingToJenkinsException extends IOException { + public NotTalkingToJenkinsException(String s) { + super(s); + } + + public NotTalkingToJenkinsException(URLConnection c) { + super("There's no Jenkins running at " + c.getURL().toString()); + } + } + /** * Flush the supplied {@link URLConnection} input and close the * connection nicely. @@ -405,6 +425,9 @@ public class CLI implements AutoCloseable { public static void main(final String[] _args) throws Exception { try { System.exit(_main(_args)); + } catch (NotTalkingToJenkinsException ex) { + System.err.println(ex.getMessage()); + System.exit(3); } catch (Throwable t) { // if the CLI main thread die, make sure to kill the JVM. t.printStackTrace(); @@ -430,6 +453,10 @@ public class CLI implements AutoCloseable { String user = null; String auth = null; + + String userIdEnv = System.getenv("JENKINS_USER_ID"); + String tokenEnv = System.getenv("JENKINS_API_TOKEN"); + boolean strictHostKey = false; while(!args.isEmpty()) { @@ -527,7 +554,7 @@ public class CLI implements AutoCloseable { for (Handler h : Logger.getLogger("").getHandlers()) { h.setLevel(level); } - for (Logger logger : new Logger[] {LOGGER, PlainCLIProtocol.LOGGER, Logger.getLogger("org.apache.sshd")}) { // perhaps also Channel + for (Logger logger : new Logger[] {LOGGER, FullDuplexHttpStream.LOGGER, PlainCLIProtocol.LOGGER, Logger.getLogger("org.apache.sshd")}) { // perhaps also Channel logger.setLevel(level); } args = args.subList(2, args.size()); @@ -541,6 +568,17 @@ public class CLI implements AutoCloseable { return -1; } + if (auth == null) { + // -auth option not set + if (StringUtils.isNotBlank(userIdEnv) && StringUtils.isNotBlank(tokenEnv)) { + auth = StringUtils.defaultString(userIdEnv).concat(":").concat(StringUtils.defaultString(tokenEnv)); + } else if (StringUtils.isNotBlank(userIdEnv) || StringUtils.isNotBlank(tokenEnv)) { + printUsage(Messages.CLI_BadAuth()); + return -1; + } // Otherwise, none credentials were set + + } + if (!url.endsWith("/")) { url += '/'; } @@ -667,13 +705,13 @@ public class CLI implements AutoCloseable { connection.sendLocale(Locale.getDefault().toString()); connection.sendStart(); connection.begin(); - final OutputStream stdin = connection.streamStdin(); new Thread("input reader") { @Override public void run() { try { + final OutputStream stdin = connection.streamStdin(); int c; - while ((c = System.in.read()) != -1) { // TODO use InputStream.available + while (!connection.complete && (c = System.in.read()) != -1) { stdin.write(c); } connection.sendEndStdin(); @@ -682,6 +720,22 @@ public class CLI implements AutoCloseable { } } }.start(); + new Thread("ping") { // JENKINS-46659 + @Override + public void run() { + try { + Thread.sleep(10_000); + while (!connection.complete) { + LOGGER.fine("sending ping"); + connection.sendEncoding(Charset.defaultCharset().name()); // no-op at this point + Thread.sleep(10_000); + } + } catch (IOException | InterruptedException x) { + LOGGER.log(Level.WARNING, null, x); + } + } + + }.start(); synchronized (connection) { while (!connection.complete) { connection.wait(); @@ -771,9 +825,14 @@ public class CLI implements AutoCloseable { return authenticate(Collections.singleton(key)); } + /** For access from {@code HelpCommand}. */ + static String usage() { + return Messages.CLI_Usage(); + } + private static void printUsage(String msg) { if(msg!=null) System.out.println(msg); - System.err.println(Messages.CLI_Usage()); + System.err.println(usage()); } static final Logger LOGGER = Logger.getLogger(CLI.class.getName()); diff --git a/cli/src/main/java/hudson/cli/Connection.java b/cli/src/main/java/hudson/cli/Connection.java index 017051a64a646a76b4ecfe81966381e3874adbca..eacc567a7e5d822afc9e4d1b20afc5473cbb0b3b 100644 --- a/cli/src/main/java/hudson/cli/Connection.java +++ b/cli/src/main/java/hudson/cli/Connection.java @@ -55,6 +55,7 @@ import java.security.Signature; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; +import org.jenkinsci.remoting.util.AnonymousClassWarnings; /** * Used by Jenkins core only in deprecated Remoting-based CLI. @@ -102,7 +103,7 @@ public class Connection { * Sends a serializable object. */ public void writeObject(Object o) throws IOException { - ObjectOutputStream oos = new ObjectOutputStream(out); + ObjectOutputStream oos = AnonymousClassWarnings.checkingObjectOutputStream(out); oos.writeObject(o); // don't close oss, which will close the underlying stream // no need to flush either, given the way oos is implemented diff --git a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java index 8017b44e4170c57c453c245db25e3beed8e4e50d..9b18142b7824257af32e510e4eb99d5cc677cf69 100644 --- a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java +++ b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java @@ -1,9 +1,7 @@ package hudson.cli; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; @@ -16,7 +14,7 @@ import org.apache.commons.codec.binary.Base64; /** * Creates a capacity-unlimited bi-directional {@link InputStream}/{@link OutputStream} pair over * HTTP, which is a request/response protocol. - * + * {@code FullDuplexHttpService} is the counterpart on the server side. * @author Kohsuke Kawaguchi */ public class FullDuplexHttpStream { @@ -29,10 +27,18 @@ public class FullDuplexHttpStream { private final OutputStream output; private final InputStream input; + /** + * A way to get data from the server. + * There will be an initial zero byte used as a handshake which you should expect and ignore. + */ public InputStream getInputStream() { return input; } + /** + * A way to upload data to the server. + * You will need to write to this and {@link OutputStream#flush} it to finish establishing a connection. + */ public OutputStream getOutputStream() { return output; } @@ -74,16 +80,15 @@ public class FullDuplexHttpStream { throw new IllegalArgumentException(relativeTarget); } - this.base = base; + this.base = tryToResolveRedirects(base, authorization); this.authorization = authorization; - URL target = new URL(base, relativeTarget); - - CrumbData crumbData = new CrumbData(); + URL target = new URL(this.base, relativeTarget); UUID uuid = UUID.randomUUID(); // so that the server can correlate those two connections // server->client + LOGGER.fine("establishing download side"); HttpURLConnection con = (HttpURLConnection) target.openConnection(); con.setDoOutput(true); // request POST to avoid caching con.setRequestMethod("POST"); @@ -92,17 +97,16 @@ public class FullDuplexHttpStream { if (authorization != null) { con.addRequestProperty("Authorization", authorization); } - if(crumbData.isValid) { - con.addRequestProperty(crumbData.crumbName, crumbData.crumb); - } con.getOutputStream().close(); input = con.getInputStream(); - // make sure we hit the right URL + // make sure we hit the right URL; no need for CLI.verifyJenkinsConnection here if (con.getHeaderField("Hudson-Duplex") == null) { - throw new IOException(target + " does not look like Jenkins, or is not serving the HTTP Duplex transport"); + throw new CLI.NotTalkingToJenkinsException("There's no Jenkins running at " + target + ", or is not serving the HTTP Duplex transport"); } + LOGGER.fine("established download side"); // calling getResponseCode or getHeaderFields breaks everything - // client->server uses chunked encoded POST for unlimited capacity. + // client->server uses chunked encoded POST for unlimited capacity. + LOGGER.fine("establishing upload side"); con = (HttpURLConnection) target.openConnection(); con.setDoOutput(true); // request POST con.setRequestMethod("POST"); @@ -113,67 +117,29 @@ public class FullDuplexHttpStream { if (authorization != null) { con.addRequestProperty ("Authorization", authorization); } - - if(crumbData.isValid) { - con.addRequestProperty(crumbData.crumbName, crumbData.crumb); - } output = con.getOutputStream(); + LOGGER.fine("established upload side"); } - static final int BLOCK_SIZE = 1024; - static final Logger LOGGER = Logger.getLogger(FullDuplexHttpStream.class.getName()); - - private final class CrumbData { - String crumbName; - String crumb; - boolean isValid; - - private CrumbData() { - this.crumbName = ""; - this.crumb = ""; - this.isValid = false; - getData(); - } - - private void getData() { - try { - String base = createCrumbUrlBase(); - String[] pair = readData(base + "?xpath=concat(//crumbRequestField,\":\",//crumb)").split(":", 2); - crumbName = pair[0]; - crumb = pair[1]; - isValid = true; - LOGGER.fine("Crumb data: "+crumbName+"="+crumb); - } catch (IOException e) { - // presumably this Hudson doesn't use crumb - LOGGER.log(Level.FINE,"Failed to get crumb data",e); - } - } - - private String createCrumbUrlBase() { - return base + "crumbIssuer/api/xml/"; - } - - private String readData(String dest) throws IOException { - HttpURLConnection con = (HttpURLConnection) new URL(dest).openConnection(); + // As this transport mode is using POST, it is necessary to resolve possible redirections using GET first. + private URL tryToResolveRedirects(URL base, String authorization) { + try { + HttpURLConnection con = (HttpURLConnection) base.openConnection(); if (authorization != null) { con.addRequestProperty("Authorization", authorization); } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()))) { - String line = reader.readLine(); - String nextLine = reader.readLine(); - if (nextLine != null) { - System.err.println("Warning: received junk from " + dest); - System.err.println(line); - System.err.println(nextLine); - while ((nextLine = reader.readLine()) != null) { - System.err.println(nextLine); - } - } - return line; - } - finally { - con.disconnect(); - } + con.getInputStream().close(); + base = con.getURL(); + } catch (Exception ex) { + // Do not obscure the problem propagating the exception. If the problem is real it will manifest during the + // actual exchange so will be reported properly there. If it is not real (no permission in UI but sufficient + // for CLI connection using one of its mechanisms), there is no reason to bother user about it. + LOGGER.log(Level.FINE, "Failed to resolve potential redirects", ex); } + return base; } + + static final int BLOCK_SIZE = 1024; + static final Logger LOGGER = Logger.getLogger(FullDuplexHttpStream.class.getName()); + } diff --git a/cli/src/main/java/hudson/cli/PlainCLIProtocol.java b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java index 17d232c87b570ab0d80069ed926a54613429451f..ed5c453360dd2475ca7605adeee13476004b694e 100644 --- a/cli/src/main/java/hudson/cli/PlainCLIProtocol.java +++ b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java @@ -34,7 +34,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.channels.ClosedChannelException; import java.nio.channels.ReadPendingException; -import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; @@ -112,13 +111,6 @@ class PlainCLIProtocol { } catch (EOFException x) { handleClose(); break; // TODO verify that we hit EOF immediately, not partway into framelen - } catch (IOException x) { - if (x.getCause() instanceof TimeoutException) { // TODO on Tomcat this seems to be SocketTimeoutException - LOGGER.log(Level.FINE, "ignoring idle timeout, perhaps from Jetty", x); - continue; - } else { - throw x; - } } if (framelen < 0) { throw new IOException("corrupt stream: negative frame length"); diff --git a/cli/src/main/java/hudson/cli/PrivateKeyProvider.java b/cli/src/main/java/hudson/cli/PrivateKeyProvider.java index d7753a750498aff78325fc04326eb63b7e45209d..9f87dd4a7b38a174229ccfc02b04779b51002951 100644 --- a/cli/src/main/java/hudson/cli/PrivateKeyProvider.java +++ b/cli/src/main/java/hudson/cli/PrivateKeyProvider.java @@ -71,7 +71,7 @@ public class PrivateKeyProvider { /** * Read keys from default keyFiles * - * .ssh/id_rsa, .ssh/id_dsa and .ssh/identity. + * {@code .ssh/id_rsa}, {@code .ssh/id_dsa} and {@code .ssh/identity}. * * @return true if some key was read successfully. */ diff --git a/cli/src/main/java/hudson/cli/SSHCLI.java b/cli/src/main/java/hudson/cli/SSHCLI.java index 8a2ecc4f388562f740e4c6a4310d9dd0ee6c4c0d..39dc9210a3e1615bac888932c689d97ad4dd1b4d 100644 --- a/cli/src/main/java/hudson/cli/SSHCLI.java +++ b/cli/src/main/java/hudson/cli/SSHCLI.java @@ -47,9 +47,11 @@ import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.future.WaitableFuture; -import org.apache.sshd.common.util.SecurityUtils; import org.apache.sshd.common.util.io.NoCloseInputStream; import org.apache.sshd.common.util.io.NoCloseOutputStream; +import org.apache.sshd.common.util.security.SecurityUtils; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Implements SSH connection mode of {@link CLI}. @@ -59,10 +61,12 @@ import org.apache.sshd.common.util.io.NoCloseOutputStream; */ class SSHCLI { + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE", justification = "Due to whatever reason FindBugs reports it fot try-with-resources") static int sshConnection(String jenkinsUrl, String user, List args, PrivateKeyProvider provider, final boolean strictHostKey) throws IOException { Logger.getLogger(SecurityUtils.class.getName()).setLevel(Level.WARNING); // suppress: BouncyCastle not registered, using the default JCE provider URL url = new URL(jenkinsUrl + "login"); URLConnection conn = url.openConnection(); + CLI.verifyJenkinsConnection(conn); String endpointDescription = conn.getHeaderField("X-SSH-Endpoint"); if (endpointDescription == null) { diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties index 921fe67a211da560ac75fc355179a117b3c00dc2..2e7fde44cb075a87813b4cced7eb48f2952c85cd 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages.properties @@ -1,22 +1,25 @@ CLI.Usage=Jenkins CLI\n\ Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ Options:\n\ - -s URL : the server URL (defaults to the JENKINS_URL env var)\n\ - -http : use a plain CLI protocol over HTTP(S) (the default; mutually exclusive with -ssh and -remoting)\n\ - -ssh : use SSH protocol (requires -user; SSH port must be open on server, and user must have registered a public key)\n\ - -remoting : use deprecated Remoting channel protocol (if enabled on server; for compatibility with legacy commands or command modes only)\n\ - -i KEY : SSH private key file used for authentication (for use with -ssh or -remoting)\n\ - -p HOST:PORT : HTTP proxy host and port for HTTPS proxy tunneling. See https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ - -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\ - -noKeyAuth : don't try to load the SSH authentication private key. Conflicts with -i\n\ - -user : specify user (for use with -ssh)\n\ - -strictHostKey : request strict host key checking (for use with -ssh)\n\ - -logger FINE : enable detailed logging from the client\n\ - -auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);\n\ - for use with -http, or -remoting but only when the JNLP agent port is disabled\n\ + \ -s URL : the server URL (defaults to the JENKINS_URL env var)\n\ + \ -http : use a plain CLI protocol over HTTP(S) (the default; mutually exclusive with -ssh and -remoting)\n\ + \ -ssh : use SSH protocol (requires -user; SSH port must be open on server, and user must have registered a public key)\n\ + \ -remoting : use deprecated Remoting channel protocol (if enabled on server; for compatibility with legacy commands or command modes only)\n\ + \ -i KEY : SSH private key file used for authentication (for use with -ssh or -remoting)\n\ + \ -p HOST:PORT : HTTP proxy host and port for HTTPS proxy tunneling. See https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ + \ -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\ + \ -noKeyAuth : don't try to load the SSH authentication private key. Conflicts with -i\n\ + \ -user : specify user (for use with -ssh)\n\ + \ -strictHostKey : request strict host key checking (for use with -ssh)\n\ + \ -logger FINE : enable detailed logging from the client\n\ + \ -auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);\n\ + \ for use with -http, or -remoting but only when the JNLP agent port is disabled.\n\ + \ Passing crendentials by a file is recommended.\n\ + \ See https://jenkins.io/redirect/cli-http-connection-mode for more info and options.\n\ \n\ - The available commands depend on the server. Run the 'help' command to\n\ + The available commands depend on the server. Run the 'help' command to \ see the list. CLI.NoURL=Neither -s nor the JENKINS_URL env var is specified. CLI.VersionMismatch=Version mismatch. This CLI cannot work with this Jenkins server. CLI.NoSuchFileExists=No such file exists: {0} +CLI.BadAuth=The JENKINS_USER_ID and JENKINS_API_TOKEN env vars should be both set or left empty. diff --git a/cli/src/main/resources/hudson/cli/client/Messages_es.properties b/cli/src/main/resources/hudson/cli/client/Messages_es.properties index f9979a5619831289dfb4952ffe328e4d6d6b9798..b8ff42b3c23b707bdd8b105f53a624e034ebcc7d 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages_es.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages_es.properties @@ -1,10 +1,24 @@ CLI.Usage=Jenkins CLI\n\ - Usar: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ - Options:\n\ - \ -s URL : direccin web (por defecto se usa la variable JENKINS_URL)\n\ + Uso: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ + Opciones:\n\ + -s URL : direcci\u00f3n web (por defecto se usa la variable JENKINS_URL)\n\ + -http : usa un protocolo plano sobre HTTP(S) (es el uso por defecto; excluyente con -ssh y -remoting)\n\ + -ssh : usa el protocolo SSH (requiere -user; el puerto SSH debe estar abierto en el servidor y el usuario debe tener registrada su clave publica)\n\ + -remoting : usa el protocolo deprecado de Remoting (siempre que est\u00e9 habilitado en el servidor; s\u00f3lo para compatibilidad con comandos heredados o legacy)\n\ + -i KEY : clave privada SSH usada para autenticaci\u00f3n (usado con -ssh o -remoting)\n\ + -p HOST:PORT : host y puerto para el uso de proxy HTTPS. Ver https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ + -noCertificateCheck : elude por completo la verificaci\u00f3n del certificado HTTPS. Usar con precauci\u00f3n\n\ + -noKeyAuth : intenta no cargar la clave privada de autenticaci\u00f3n SSH. Presenta conflicto con -i\n\ + -user : especifica el usuario (se usa con -ssh)\n\ + -strictHostKey : solicita la comprobaci\u00f3n estricta de la clave del host (se usa con -ssh)\n\ + -logger FINE : habilita logs detallados en el cliente\n\ + -auth [ USER:SECRET | @FILE ] : especifica el usuario y contrase\u00f1a o API token (o los carga de un fichero);\n\ + se usa con -http o -remoting pero s\u00f3lo si el puerto del agente JNLP est\u00e1 deshabilitado.\n\ + No es necesario si las variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN se encuentran configuradas.\n\ \n\ - La lista completa de comandos disponibles depende del servidor. Ejecuta\n\ - el comando ''help'' para ver la lista. -CLI.NoURL=No se ha especificado el parmetro -s ni la variable JENKINS_URL -CLI.VersionMismatch=La versin no coincide. Esta consola "CLI" no se puede usar en este Jenkins + La lista de comandos disponibles depende del servidor. Ejecuta\n\ + el comando ''help'' para ver la lista completa. +CLI.NoURL=No se ha especificado el par\u00e1metro -s ni la variable JENKINS_URL +CLI.VersionMismatch=La versi\u00f3n no coincide. Esta consola "CLI" no se puede usar en este Jenkins CLI.NoSuchFileExists=El fichero {0} no existe. +CLI.BadAuth=Ambas variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN deber\u00edan estar configuradas o mantenerse vac\u00edas. diff --git a/cli/src/main/resources/hudson/cli/client/Messages_it.properties b/cli/src/main/resources/hudson/cli/client/Messages_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..6236107d9c7cff3786ea619aba6097291835d6f7 --- /dev/null +++ b/cli/src/main/resources/hudson/cli/client/Messages_it.properties @@ -0,0 +1,22 @@ +CLI.NoSuchFileExists=File non esistente: {0} +CLI.NoURL=Non sono stati specificati n -s n la variabile d''ambiente JENKINS_URL. +CLI.VersionMismatch=Le versioni non corrispondono. Quest''interfaccia della riga di comando non pu funzionare con questo server Jenkins. +CLI.Usage=Interfaccia della riga di comando di Jenkins\n\ +Uso: java -jar jenkins-cli.jar [-s URL] comando [opzioni...] argomenti...\n\ +Opzioni:\n\ +-s URL : l''URL del server (impostazione predefinita: la variabile d''ambiente JENKINS_URL)\n\ +-http : utilizza un protocollo interfaccia della riga di comando in testo semplice su HTTP(S) (impostazione predefinita; mutualmente esclusiva con -ssh e -remoting)\n\ +-ssh : utilizza il protocollo SSH (richiede -user; la porta SSH deve essere aperta sul server e l''utente deve aver registrato una chiave pubblica)\n\ +-remoting : utilizza il protocollo deprecato Remoting channel (se abilitato sul server; solo per compatibilit con comandi o modalit comandi legacy)\n\ +-i KEY : file chiave privata SSH utilizzato per l''autenticazione (per l''utilizzo con -ssh o -remoting)\n\ +-p HOST:PORTA : host proxy HTTP e porta per il tunneling proxy HTTPS. Vedi https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ +-noCertificateCheck : ometti completamente il controllo dei certificati HTTPS. Utilizzare con cautela\n\ +-noKeyAuth : non tentare di caricare la chiave privata per l''autenticazione SSH. In conflitto con -i\n\ +-user : specifica l''utente (per l''utilizzo con -ssh)\n\ +-strictHostKey : richiedi la modalit strict per il controllo delle chiavi host (per l''utilizzo con -ssh)\n\ +-logger FINE : abilita la registrazione dettagliata da parte del client\n\ +-auth [ UTENTE:SEGRETO | @FILE ] : specifica il nome utente e la password o il token API (o caricali entrambi da un file);\n\ +per l''utilizzo con -http, o -remoting ma solo quando la porta agente JNLP disabilitata\n\ +\n\ +I comandi disponibili dipendono dal server. Eseguire il comando ''help'' per\n\ +visualizzarne l''elenco. diff --git a/cli/src/main/resources/hudson/cli/client/Messages_zh_CN.properties b/cli/src/main/resources/hudson/cli/client/Messages_zh_CN.properties new file mode 100644 index 0000000000000000000000000000000000000000..16fa41c48c9a4aad3d63e9f40e2d017b3bd9fb77 --- /dev/null +++ b/cli/src/main/resources/hudson/cli/client/Messages_zh_CN.properties @@ -0,0 +1,44 @@ +# The MIT License +# +# Copyright (c) 2013, Sun Microsystems, Inc., Kohsuke Kawaguchi, Pei-Tang Huang, +# Chunghwa Telecom Co., Ltd., and a number of other of contributers +# +# 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. + +CLI.Usage=Jenkins CLI\n\ + \u7528\u6cd5: java -jar jenkins-cli.jar [-s url] \u547d\u4ee4 [\u9009\u9879...] \u53c2\u6570...\n\ + \u9009\u9879:\n\ + -s URL : \u670d\u52a1\u5668url \uff08\u9ed8\u8ba4\u662fjenkins_url\u73af\u5883\u53d8\u91cf\uff09\n\ + -http : \u5728http%28s%29\u4e0a\u4f7f\u7528\u539f\u59cb\u7684cli\u534f\u8bae\uff08\u9ed8\u8ba4\u503c%3b \u4e0e -ssh \u548c -remoting \u4e92\u65a5\uff09\n\ + -ssh : \u4f7f\u7528ssh\u534f\u8bae\uff08\u9700\u8981 -user%3b \u670d\u52a1\u5668\u7684ssh\u7aef\u53e3\u5fc5\u987b\u6253\u5f00\uff0c\u4e14\u7528\u6237\u5fc5\u987b\u5df2\u6ce8\u518c\u516c\u94a5\u3002\uff09\n\ + -remoting : \u4f7f\u7528\u4e0d\u63a8\u8350\u7684\u8fdc\u7a0b\u4fe1\u9053\u534f\u8bae \uff08\u5982\u679c\u670d\u52a1\u5668\u4e0a\u662f\u6253\u5f00\u7684\uff1b\u4ec5\u7528\u4e8e\u517c\u5bb9\u9057\u7559\u547d\u4ee4\u6216\u547d\u4ee4\u6a21\u5f0f\uff09\n\ + -i KEY : \u7528\u4e8e\u8ba4\u8bc1\u7684ssh\u79c1\u94a5\u6587\u4ef6\uff08\u4e0e -ssh \u6216 -remoting \u4e00\u8d77\u4f7f\u7528\uff09\n\ + -p HOST:PORT : \u7528\u6237https\u4ee3\u7406\u96a7\u9053\u7684http\u4ee3\u7406\u4e3b\u673a\u548c\u7aef\u53e3\u3002\u53c2\u89c1 https://jenkins.io/redirect/cli-https-proxy-tunnel\n\ + -noCertificateCheck : \u5b8c\u5168\u5ffd\u7565https\u8bc1\u4e66\u8ba4\u8bc1\u3002\u8c28\u614e\u4f7f\u7528\n\ + -noKeyAuth : \u65e0\u9700\u5c1d\u8bd5\u52a0\u8f7dssh\u8ba4\u8bc1\u79c1\u94a5\u3002\u4e0e -i \u76f8\u53cd\n\ + -user : \u6307\u5b9a\u7528\u6237\uff08\u4e0e -ssh \u4e00\u8d77\u4f7f\u7528\uff09\n\ + -strictHostKey : \u8981\u6c42\u9a8c\u8bc1\u4e3b\u673akey\u68c0\u67e5\uff08\u4e0e -ssh \u4e00\u8d77\u4f7f\u7528\uff09\n\ + -logger FINE : \u5141\u8bb8\u5ba2\u6237\u7aef\u8be6\u7ec6\u65e5\u5fd7\u8bb0\u5f55\n\ + -auth [ USER:SECRET | @FILE ] : \u6307\u5b9a\u7528\u6237\u540d\u4e0e\u5bc6\u7801\u6216\u7528\u6237\u540d\u4e0eapi token\uff08\u6216\u8005\u4ece\u6587\u4ef6\u52a0\u8f7d\uff09\uff1b\n\ + \u4e0e -http \u4e00\u8d77\u4f7f\u7528\uff0c\u6216\u8005\u53ea\u5728jnlp\u4ee3\u7406\u7aef\u53e3\u7981\u7528\u65f6\u4e0e -remoting \u4e00\u8d77\u4f7f\u7528\n\ + \n\ + \u53ef\u7528\u7684\u547d\u4ee4\u53d6\u51b3\u4e8e\u670d\u52a1\u5668\u3002\u6267\u884c 'help' \u547d\u4ee4\u53ef\u4ee5\u67e5\u770b\u5b8c\u6574\u6e05\u5355\u3002 +CLI.NoURL=\u6c92\u6709\u6307\u5b9a -s \u53c2\u6570\u6216\u8005 jenkins_url \u73af\u5883\u53d8\u91cf\u3002 +CLI.VersionMismatch=\u7248\u672c\u4e0d\u5339\u914d\u3002cli \u65e0\u6cd5\u5728\u6b64 jenkins \u670d\u52a1\u5668\u4e0a\u8fd0\u884c\u3002 +CLI.NoSuchFileExists=\u6587\u4ef6\u4e0d\u5b58\u5728: {0} diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java index 86970265089b4bf41cb7bc4aae7d6e320447d21a..a29af91e6eb818245140e987dda1c323786a3fd8 100644 --- a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java +++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java @@ -47,7 +47,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; @RunWith(PowerMockRunner.class) -@PrepareForTest(CLI.class) // When mocking new operator caller has to be @PreparedForTest, not class itself +@PrepareForTest({CLI.class, CLIConnectionFactory.class}) // When mocking new operator caller has to be @PreparedForTest, not class itself public class PrivateKeyProviderTest { @Test @@ -113,15 +113,9 @@ public class PrivateKeyProviderTest { private Iterable withKeyPairs(final KeyPair... expected) { return Mockito.argThat(new ArgumentMatcher>() { - @Override public void describeTo(Description description) { - description.appendText(Arrays.asList(expected).toString()); - } - - @Override public boolean matches(Object argument) { - if (!(argument instanceof Iterable)) throw new IllegalArgumentException("Not an instance of Iterrable"); - @SuppressWarnings("unchecked") - final Iterable actual = (Iterable) argument; + @Override + public boolean matches(Iterable actual) { int i = 0; for (KeyPair akp: actual) { if (!eq(expected[i].getPublic(), akp.getPublic())) return false; diff --git a/core/pom.xml b/core/pom.xml index 3c5826db0ee92169bce8a564a3ce4057c26485f2..4ba0498ae9a876d95afc42bad9b0b84eb47eda0a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -28,8 +28,8 @@ THE SOFTWARE. org.jenkins-ci.main - pom - 2.68-SNAPSHOT + jenkins-parent + ${revision}${changelist} jenkins-core @@ -39,11 +39,9 @@ THE SOFTWARE. true - 1.250 + 1.256 2.5.6.SEC03 - 2.4.11 - - true + 2.4.12 @@ -95,6 +93,12 @@ THE SOFTWARE. com.google.inject guice + + + com.google.guava + guava + + @@ -105,7 +109,7 @@ THE SOFTWARE. com.github.jnr jnr-posix - 3.0.41 + 3.0.45 org.kohsuke @@ -115,7 +119,7 @@ THE SOFTWARE. org.jenkins-ci trilead-ssh2 - build-217-jenkins-11 + build-217-jenkins-14 org.kohsuke.stapler @@ -173,6 +177,17 @@ THE SOFTWARE. tests test + + io.jenkins.stapler + jenkins-stapler-support + 1.0 + + + org.hamcrest + hamcrest-library + 1.3 + test + com.infradna.tool @@ -197,17 +212,16 @@ THE SOFTWARE. org.jenkins-ci annotation-indexer - 1.12 org.jenkins-ci bytecode-compatibility-transformer - 1.8 + 2.0-beta-2 org.jenkins-ci task-reactor - 1.4 + 1.5 org.jvnet.localizer @@ -234,6 +248,20 @@ THE SOFTWARE. + + + xpp3 + xpp3 + 1.1.4c + + + net.sf.kxml + kxml2 + 2.3.0 + jfree jfreechart @@ -256,7 +284,6 @@ THE SOFTWARE. commons-lang commons-lang - 2.6 commons-digester @@ -440,11 +467,6 @@ THE SOFTWARE. spring-aop ${spring.version} - - xpp3 - xpp3 - 1.1.4c - junit junit @@ -462,13 +484,13 @@ THE SOFTWARE. org.powermock - powermock-api-mockito + powermock-api-mockito2 test - javax.servlet - jstl - 1.1.0 + javax.servlet.jsp.jstl + javax.servlet.jsp.jstl-api + 1.2.1 org.slf4j @@ -496,7 +518,7 @@ THE SOFTWARE. org.jvnet.winp winp - 1.25 + 1.27 org.jenkins-ci @@ -516,7 +538,7 @@ THE SOFTWARE. net.java.dev.jna jna - 4.2.1 + 4.5.2 org.kohsuke @@ -526,7 +548,7 @@ THE SOFTWARE. org.kohsuke libpam4j - 1.8 + 1.11 org.kohsuke @@ -541,7 +563,7 @@ THE SOFTWARE. net.java.sezpoz sezpoz - 1.12 + 1.13 org.kohsuke.jinterop @@ -551,8 +573,7 @@ THE SOFTWARE. org.kohsuke.metainf-services metainf-services - 1.4 - provided + 1.8 true @@ -566,10 +587,9 @@ THE SOFTWARE. 1.1 - + commons-codec commons-codec - 1.8 @@ -580,14 +600,13 @@ THE SOFTWARE. com.google.code.findbugs annotations - 3.0.0 provided commons-fileupload commons-fileupload - 1.3.1-jenkins-1 + 1.3.1-jenkins-2 @@ -602,6 +621,12 @@ THE SOFTWARE. com.google.guava guava + + + com.google.code.findbugs + jsr305 + + com.google.guava @@ -753,7 +778,7 @@ THE SOFTWARE. com.sun.winsw winsw - 2.1.0 + 2.2.0 bin exe ${project.build.outputDirectory}/windows-service @@ -785,25 +810,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - - - - - testCompile - - - - - - org.codehaus.groovy - groovy-all - ${groovy.version} - - - org.codehaus.mojo findbugs-maven-plugin @@ -878,41 +884,5 @@ THE SOFTWARE. true - - - cobertura - - - - org.codehaus.gmaven - gmaven-plugin - - - - - test - - execute - - - - ${project.basedir}/src/build-script - - ${project.basedir}/src/build-script/unitTest.groovy - - - - - - - maven-surefire-plugin - - - true - - - - - diff --git a/core/src/build-script/Cobertura.groovy b/core/src/build-script/Cobertura.groovy deleted file mode 100644 index 49b773b49ba17e79fa74975cf8a0b20d42153143..0000000000000000000000000000000000000000 --- a/core/src/build-script/Cobertura.groovy +++ /dev/null @@ -1,82 +0,0 @@ -import org.apache.maven.project.MavenProject; - -/** - * Cobertura invoker. - */ -public class Cobertura { - private final MavenProject project; - // maven helper - private def maven; - // ant builder - private def ant; - /** - * Cobertura data file. - */ - private final File ser; - - def Cobertura(project, maven, ant, ser) { - this.project = maven.project; - this.maven = maven; - this.ant = ant; - this.ser =ser; - - // define cobertura tasks - ant.taskdef(resource:"tasks.properties") - } - - // function that ensures that the given directory exists - private String dir(String dir) { - new File(project.basedir,dir).mkdirs(); - return dir; - } - - /** - * Instruments the given class dirs/jars by cobertura - * - * @param files - * List of jar files and class dirs to instrument. - */ - def instrument(files) { - ant."cobertura-instrument"(todir:dir("target/cobertura-classes"),datafile:ser) { - fileset(dir:"target/classes"); - files.each{ fileset(file:it) } - } - } - - def runTests() { - ant.junit(fork:true, forkMode:"once", failureproperty:"failed", printsummary:true) { - classpath { - junitClasspath() - } - batchtest(todir:dir("target/surefire-reports")) { - fileset(dir:"target/test-classes") { - include(name:"**/*Test.class") - } - formatter(type:"xml") - } - sysproperty(key:"net.sourceforge.cobertura.datafile",value:ser) - sysproperty(key:"hudson.ClassicPluginStrategy.useAntClassLoader",value:"true") - } - } - - def junitClasspath() { - ant.pathelement(path: "target/cobertura-classes") // put the instrumented classes first - ant.fileset(dir:"target/cobertura-classes",includes:"*.jar") // instrumented jar files - ant.pathelement(path: maven.resolveArtifact("net.sourceforge.cobertura:cobertura:1.9")) // cobertura runtime - project.getTestClasspathElements().each { ant.pathelement(path: it) } // the rest of the dependencies - } - - def report(dirs) { - maven.attachArtifact(ser,"ser","cobertura") - ["html","xml"].each { format -> - ant."cobertura-report"(format:format,datafile:ser,destdir:dir("target/cobertura-reports"),srcdir:"src/main/java") { - dirs.each{ fileset(dir:it) } - } - } - } - - def makeBuildFailIfTestFail() { - if(ant.project.getProperty("failed")!=null && !Boolean.getBoolean("testFailureIgnore")) - throw new Exception("Some unit tests failed"); - } -} diff --git a/core/src/build-script/readme.txt b/core/src/build-script/readme.txt deleted file mode 100644 index 6c53a01026b7921e493e1c42f7463c0857c17c69..0000000000000000000000000000000000000000 --- a/core/src/build-script/readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -Parts of the build scripts that are written in Groovy. - -IntelliJ needs Groovy files to be in its source directory to provide completion and such, -so we need this to be in its own directory. \ No newline at end of file diff --git a/core/src/build-script/unitTest.groovy b/core/src/build-script/unitTest.groovy deleted file mode 100644 index 653391aad7f15bbc6cdea2366f5f143bd4a0ae2b..0000000000000000000000000000000000000000 --- a/core/src/build-script/unitTest.groovy +++ /dev/null @@ -1,11 +0,0 @@ -// run unit tests - -ant.project.setBaseDir(project.basedir) -ser=new File(project.basedir,"target/cobertura.ser"); // store cobertura data file in a module-specific location - -cob = new Cobertura(project,maven,ant,ser); - -cob.instrument([]) -cob.runTests() -cob.report([]) -cob.makeBuildFailIfTestFail(); diff --git a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties b/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties index 7378e405c76f9e21a930a2e698b8baf8f024a484..510a7f5f861f964e339da3ac0755dab0a1779d10 100644 --- a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties +++ b/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties @@ -1,27 +1,6 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Giulio D'Ambrosi -# -# 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. - -NewVersionAvailable=C''\u00E8 una nuova versione di Jenkins ({0}) disponibile per il download (changelog). -Or\ Upgrade\ Automatically=Oppure aggiorna automaticamente -UpgradeComplete=Aggiornamento a Jenkins {0} completato, in attesa di riavvio. -UpgradeCompleteRestartNotSupported=Aggiornamento a Jenkins {0} completato, in attesa di riavvio. -UpgradeProgress=Aggiornamento a Jenkins {0} in corso oppure fallito. +NewVersionAvailable=Una nuova versione di Jenkins ({0}) disponibile per il download \ + (log delle modifiche). +UpgradeComplete=Aggiornamento a Jenkins {0} completato, in attesa del riavvio. +UpgradeCompleteRestartNotSupported=Aggiornamento a Jenkins {0} completato, in attesa del riavvio. +UpgradeProgress=Aggiornamento a Jenkins {0} in corso. +UpgradeFailed=Aggiornamento a Jenkins {0} non riuscito: {1}. diff --git a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_CN.properties b/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_CN.properties deleted file mode 100644 index 0ef79b530b974daaaed8ae9bc346ed86fb71a2bc..0000000000000000000000000000000000000000 --- a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_CN.properties +++ /dev/null @@ -1,6 +0,0 @@ -# This file is under the MIT License by authors - -NewVersionAvailable=Jenkins\u65B0\u7248\u672C ({0})\u53EF\u70B9\u51FB download (\u53D8\u66F4\u8BF4\u660E)\u4E0B\u8F7D\u3002 -Or\ Upgrade\ Automatically=\u6216 \u81EA\u52A8\u5347\u7EA7\u7248\u672C -UpgradeComplete=Jenkins {0} \u7248\u672C\u5347\u7EA7\u5DF2\u5B8C\u6210,\u7B49\u5F85\u91CD\u542F\u4E2D. -UpgradeCompleteRestartNotSupported=Jenkins {0} \u7248\u672C\u5347\u7EA7\u5DF2\u5B8C\u6210,\u7B49\u5F85\u91CD\u542F\u4E2D. diff --git a/core/src/filter/resources/hudson/model/hudson-version.properties b/core/src/filter/resources/hudson/model/hudson-version.properties index 9b9343d10d659357b7acccbcf34bc2aece573625..defbd48204e4784090fe0e17e28fe5bc395abf47 100644 --- a/core/src/filter/resources/hudson/model/hudson-version.properties +++ b/core/src/filter/resources/hudson/model/hudson-version.properties @@ -1 +1 @@ -version=${build.version} \ No newline at end of file +version=${project.version} diff --git a/core/src/filter/resources/jenkins/model/jenkins-version.properties b/core/src/filter/resources/jenkins/model/jenkins-version.properties index 9b9343d10d659357b7acccbcf34bc2aece573625..defbd48204e4784090fe0e17e28fe5bc395abf47 100644 --- a/core/src/filter/resources/jenkins/model/jenkins-version.properties +++ b/core/src/filter/resources/jenkins/model/jenkins-version.properties @@ -1 +1 @@ -version=${build.version} \ No newline at end of file +version=${project.version} diff --git a/core/src/filter/resources/jenkins/slaves/remoting-info.properties b/core/src/filter/resources/jenkins/slaves/remoting-info.properties new file mode 100644 index 0000000000000000000000000000000000000000..6ac78f4ad43e96e9f1277206b67049f098979a21 --- /dev/null +++ b/core/src/filter/resources/jenkins/slaves/remoting-info.properties @@ -0,0 +1,6 @@ +# Remoting version, which is embedded into the core +# This version MAY differ from what is really classloaded (see the "Pluggable Core Components", JENKINS-41196) +remoting.embedded.version=${remoting.version} + +# Minimum Remoting version on external agents which is supported by the core +remoting.minimum.supported.version=${remoting.minimum.supported.version} diff --git a/core/src/main/groovy/hudson/util/LoadMonitor.groovy b/core/src/main/groovy/hudson/util/LoadMonitor.groovy deleted file mode 100644 index 97d701816199c011e67c6fcc39e539563442f225..0000000000000000000000000000000000000000 --- a/core/src/main/groovy/hudson/util/LoadMonitor.groovy +++ /dev/null @@ -1,116 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.util - -import hudson.model.Computer -import jenkins.util.Timer -import jenkins.model.Jenkins -import hudson.model.Label -import hudson.model.Queue.BlockedItem -import hudson.model.Queue.BuildableItem -import hudson.model.Queue.WaitingItem -import hudson.triggers.SafeTimerTask -import java.text.DateFormat -import java.util.concurrent.TimeUnit - -/** - * Spits out the load information. - * - *

- * I'm using this code to design the auto scaling feature. - * In future this might be useful data to expose to the UI. - * - * @author Kohsuke Kawaguchi - */ -public class LoadMonitorImpl extends SafeTimerTask { - - private final File dataFile; - private List labels; - - public LoadMonitorImpl(File dataFile) { - this.dataFile = dataFile; - labels = Jenkins.getInstance().labels*.name; - printHeaders(); - Timer.get().scheduleAtFixedRate(this,0,10*1000, TimeUnit.MILLISECONDS); - } - - private String quote(Object s) { "\"${s}\""; } - - protected void printHeaders() { - def headers = ["# of executors","# of busy executors","BuildableItems in Q","BuildableItem avg wait time"]; - def data = ["timestamp"]; - data += headers; - data += ["WaitingItems in Q","BlockedItems in Q"]; - - for( String label : labels) - data += headers.collect { "${it} (${label}}" } - - dataFile.append(data.collect({ quote(it) }).join(",")+"\n"); - } - - @Override - protected void doRun() { - def now = new Date(); - def data = []; - data.add(quote(FORMATTER.format(now))); - - def h = Jenkins.getInstance(); - - def items = h.queue.items; - def filterByType = {Class type -> items.findAll { type.isInstance(it) } } - - def builder = {List cs, Closure itemFilter -> - // number of total executor, number of busy executor - data.add(cs.sum { it.isOffline() ? 0 : it.numExecutors }); - data.add(cs.sum {Computer c -> - c.executors.findAll { !it.isIdle() }.size() - }); - - // queue statistics - def is = filterByType(BuildableItem).findAll(itemFilter); - data.add(is.size()); - data.add(is.sum {BuildableItem bi -> now.time - bi.buildableStartMilliseconds }?:0 / Math.max(1,is.size()) ); - }; - - - // for the whole thing - builder(Arrays.asList(h.computers),{ it->true }); - - data.add(filterByType(WaitingItem).size()); - data.add(filterByType(BlockedItem).size()); - - // per label stats - for (String label : labels) { - Label l = h.getLabel(label) - builder(l.nodes.collect { it.toComputer() }) { BuildableItem bi -> bi.task.assignedLabel==l }; - } - - dataFile.append(data.join(",")+"\n"); - } - - private static final DateFormat FORMATTER = DateFormat.getDateTimeInstance(); -} - -new LoadMonitorImpl(new File("/files/hudson/load.txt")); - diff --git a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy b/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy deleted file mode 100644 index 08cb1c23a3da552cc531cf3b655b0a915b43aee6..0000000000000000000000000000000000000000 --- a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy +++ /dev/null @@ -1,47 +0,0 @@ -package jenkins.util.groovy - -import lib.FormTagLib -import lib.LayoutTagLib -import org.kohsuke.stapler.jelly.groovy.JellyBuilder -import org.kohsuke.stapler.jelly.groovy.Namespace -import lib.JenkinsTagLib - -/** - * Base class for utility classes for Groovy view scripts - *

- * Usage from script of a subclass, say ViewHelper: - *

- * new ViewHelper(delegate).method(); - *

- * see ModularizeViewScript in ui-samples for an example how to use this class. - * - * @author Stefan Wolf (wolfs) - */ -abstract class AbstractGroovyViewModule { - JellyBuilder builder - FormTagLib f - LayoutTagLib l - JenkinsTagLib t - Namespace st - - public AbstractGroovyViewModule(JellyBuilder b) { - builder = b - f= builder.namespace(FormTagLib) - l=builder.namespace(LayoutTagLib) - t=builder.namespace(JenkinsTagLib) - st=builder.namespace("jelly:stapler") - - } - - def methodMissing(String name, args) { - builder.invokeMethod(name,args) - } - - def propertyMissing(String name) { - builder.getProperty(name) - } - - def propertyMissing(String name, value) { - builder.setProperty(name, value) - } -} diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index 5981ade06904c23de2c75603ec328476db760fe3..5d173c3839022d162265f7b2a2779c436e30791a 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -23,10 +23,15 @@ */ package hudson; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import java.io.FileNotFoundException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.InvalidPathException; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jenkins.util.AntWithFindResourceClassLoader; import jenkins.util.SystemProperties; import com.google.common.collect.Lists; @@ -58,25 +63,24 @@ import org.apache.tools.zip.ZipOutputStream; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; -import java.lang.reflect.Field; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.Vector; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jenkinsci.bytecode.Transformer; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -109,11 +113,20 @@ public class ClassicPluginStrategy implements PluginStrategy { @Override public String getShortName(File archive) throws IOException { Manifest manifest; + if (!archive.exists()) { + throw new FileNotFoundException("Failed to load " + archive + ". The file does not exist"); + } else if (!archive.isFile()) { + throw new FileNotFoundException("Failed to load " + archive + ". It is not a file"); + } + if (isLinked(archive)) { manifest = loadLinkedManifest(archive); } else { try (JarFile jf = new JarFile(archive, false)) { manifest = jf.getManifest(); + } catch (IOException ex) { + // Mention file name in the exception + throw new IOException("Failed to load " + archive, ex); } } return PluginWrapper.computeShortName(manifest, archive.getName()); @@ -268,7 +281,7 @@ public class ClassicPluginStrategy implements PluginStrategy { if (detached.shortName.equals(pluginName)) { continue; } - if (BREAK_CYCLES.contains(pluginName + '/' + detached.shortName)) { + if (BREAK_CYCLES.contains(pluginName + ' ' + detached.shortName)) { LOGGER.log(Level.FINE, "skipping implicit dependency {0} → {1}", new Object[] {pluginName, detached.shortName}); continue; } @@ -408,35 +421,37 @@ public class ClassicPluginStrategy implements PluginStrategy { public VersionNumber getRequiredVersion() { return new VersionNumber(requiredVersion); } + + @Override + public String toString() { + return shortName + " " + splitWhen.toString().replace(".*", "") + " " + requiredVersion; + } } - private static final List DETACHED_LIST = Collections.unmodifiableList(Arrays.asList( - new DetachedPlugin("maven-plugin", "1.296", "1.296"), - new DetachedPlugin("subversion", "1.310", "1.0"), - new DetachedPlugin("cvs", "1.340", "0.1"), - new DetachedPlugin("ant", "1.430.*", "1.0"), - new DetachedPlugin("javadoc", "1.430.*", "1.0"), - new DetachedPlugin("external-monitor-job", "1.467.*", "1.0"), - new DetachedPlugin("ldap", "1.467.*", "1.0"), - new DetachedPlugin("pam-auth", "1.467.*", "1.0"), - new DetachedPlugin("mailer", "1.493.*", "1.2"), - new DetachedPlugin("matrix-auth", "1.535.*", "1.0.2"), - new DetachedPlugin("windows-slaves", "1.547.*", "1.0"), - new DetachedPlugin("antisamy-markup-formatter", "1.553.*", "1.0"), - new DetachedPlugin("matrix-project", "1.561.*", "1.0"), - new DetachedPlugin("junit", "1.577.*", "1.0"), - new DetachedPlugin("bouncycastle-api", "2.16.*", "2.16.0") - )); + /** Record of which plugins which removed from core and when. */ + private static final List DETACHED_LIST; /** Implicit dependencies that are known to be unnecessary and which must be cut out to prevent a dependency cycle among bundled plugins. */ - private static final Set BREAK_CYCLES = new HashSet(Arrays.asList( - "script-security/matrix-auth", - "script-security/windows-slaves", - "script-security/antisamy-markup-formatter", - "script-security/matrix-project", - "credentials/matrix-auth", - "credentials/windows-slaves" - )); + private static final Set BREAK_CYCLES; + + static { + try (InputStream is = ClassicPluginStrategy.class.getResourceAsStream("/jenkins/split-plugins.txt")) { + DETACHED_LIST = ImmutableList.copyOf(configLines(is).map(line -> { + String[] pieces = line.split(" "); + return new DetachedPlugin(pieces[0], pieces[1] + ".*", pieces[2]); + }).collect(Collectors.toList())); + } catch (IOException x) { + throw new ExceptionInInitializerError(x); + } + try (InputStream is = ClassicPluginStrategy.class.getResourceAsStream("/jenkins/split-plugin-cycles.txt")) { + BREAK_CYCLES = ImmutableSet.copyOf(configLines(is).collect(Collectors.toSet())); + } catch (IOException x) { + throw new ExceptionInInitializerError(x); + } + } + private static Stream configLines(InputStream is) throws IOException { + return org.apache.commons.io.IOUtils.readLines(is, StandardCharsets.UTF_8).stream().filter(line -> !line.matches("#.*|\\s*")); + } /** * Computes the classloader that takes the class masking into account. @@ -764,19 +779,20 @@ public class ClassicPluginStrategy implements PluginStrategy { Class c = ClassLoaderReflectionToolkit._findLoadedClass(pw.classLoader, name); if (c!=null) return c; return ClassLoaderReflectionToolkit._findClass(pw.classLoader, name); - } catch (ClassNotFoundException e) { + } catch (ClassNotFoundException ignored) { //not found. try next } } } else { for (Dependency dep : dependencies) { PluginWrapper p = pluginManager.getPlugin(dep.shortName); - if(p!=null) + if(p!=null) { try { return p.classLoader.loadClass(name); - } catch (ClassNotFoundException _) { - // try next + } catch (ClassNotFoundException ignored) { + // OK, try next } + } } } @@ -784,6 +800,8 @@ public class ClassicPluginStrategy implements PluginStrategy { } @Override + @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", + justification = "Should not produce network overheads since the URL is local. JENKINS-53793 is a follow-up") protected Enumeration findResources(String name) throws IOException { HashSet result = new HashSet(); diff --git a/core/src/main/java/hudson/CopyOnWrite.java b/core/src/main/java/hudson/CopyOnWrite.java index efe21357c4405e122651a284c0cb4c30157fce62..891cbf04bb9ffef6d6f657d05eb4708d94fa0f5e 100644 --- a/core/src/main/java/hudson/CopyOnWrite.java +++ b/core/src/main/java/hudson/CopyOnWrite.java @@ -43,7 +43,7 @@ import java.lang.annotation.Target; * *

* The field marked with this annotation usually needs to be marked as - * volatile. + * {@code volatile}. * * @author Kohsuke Kawaguchi */ diff --git a/core/src/main/java/hudson/DescriptorExtensionList.java b/core/src/main/java/hudson/DescriptorExtensionList.java index 16d96ae441b0ff5a7cc4a3e59e8398db0cc595a4..8452227e5e9467b1442fbe2b611c4a32e582df56 100644 --- a/core/src/main/java/hudson/DescriptorExtensionList.java +++ b/core/src/main/java/hudson/DescriptorExtensionList.java @@ -31,7 +31,6 @@ import jenkins.model.Jenkins; import hudson.model.ViewDescriptor; import hudson.model.Descriptor.FormException; import hudson.util.AdaptedIterator; -import hudson.util.Memoizer; import hudson.util.Iterators.FlattenIterator; import hudson.slaves.NodeDescriptor; import hudson.tasks.Publisher; @@ -41,6 +40,8 @@ import java.util.List; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.concurrent.CopyOnWriteArrayList; @@ -160,13 +161,13 @@ public class DescriptorExtensionList, D extends Descrip @Override public boolean add(D d) { boolean r = super.add(d); - hudson.getExtensionList(Descriptor.class).add(d); + getDescriptorExtensionList().add(d); return r; } @Override public boolean remove(Object o) { - hudson.getExtensionList(Descriptor.class).remove(o); + getDescriptorExtensionList().remove(o); return super.remove(o); } @@ -175,7 +176,8 @@ public class DescriptorExtensionList, D extends Descrip */ @Override protected Object getLoadLock() { - return this; + // Get a lock for the singleton extension list to prevent deadlocks (JENKINS-55361) + return getDescriptorExtensionList().getLoadLock(); } /** @@ -188,7 +190,7 @@ public class DescriptorExtensionList, D extends Descrip LOGGER.log(Level.WARNING, "Cannot load extension components, because Jenkins instance has not been assigned yet"); return Collections.emptyList(); } - return _load(jenkins.getExtensionList(Descriptor.class).getComponents()); + return _load(getDescriptorExtensionList().getComponents()); } @Override @@ -210,17 +212,19 @@ public class DescriptorExtensionList, D extends Descrip return r; } + private ExtensionList getDescriptorExtensionList() { + return ExtensionList.lookup(Descriptor.class); + } + /** * Stores manually registered Descriptor instances. Keyed by the {@link Describable} type. */ - private static final Memoizer>> legacyDescriptors = new Memoizer>>() { - public CopyOnWriteArrayList compute(Class key) { - return new CopyOnWriteArrayList(); - } - }; + @SuppressWarnings("rawtypes") + private static final Map>> legacyDescriptors = new ConcurrentHashMap<>(); + @SuppressWarnings({"unchecked", "rawtypes"}) private static > CopyOnWriteArrayList>> getLegacyDescriptors(Class type) { - return (CopyOnWriteArrayList)legacyDescriptors.get(type); + return legacyDescriptors.computeIfAbsent(type, key -> new CopyOnWriteArrayList()); } /** diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java index 1849ecbf66aebaf6ca6a4df5ce197b5b825c8b36..a24976b219169c690bba916faa467f4c0a1d4bdf 100644 --- a/core/src/main/java/hudson/EnvVars.java +++ b/core/src/main/java/hudson/EnvVars.java @@ -44,6 +44,7 @@ import java.util.TreeSet; import java.util.UUID; import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.CheckForNull; /** * Environment variables. @@ -54,7 +55,7 @@ import javax.annotation.Nonnull; * but case insensitive way (that is, cmd.exe can get both FOO and foo as environment variables * when it's launched, and the "set" command will display it accordingly, but "echo %foo%" results in * echoing the value of "FOO", not "foo" — this is presumably caused by the behavior of the underlying - * Win32 API GetEnvironmentVariable acting in case insensitive way.) Windows users are also + * Win32 API {@code GetEnvironmentVariable} acting in case insensitive way.) Windows users are also * used to write environment variable case-insensitively (like %Path% vs %PATH%), and you can see many * documents on the web that claims Windows environment variables are case insensitive. * @@ -65,10 +66,10 @@ import javax.annotation.Nonnull; *

* In Jenkins, often we need to build up "environment variable overrides" * on master, then to execute the process on agents. This causes a problem - * when working with variables like PATH. So to make this work, - * we introduce a special convention PATH+FOO — all entries - * that starts with PATH+ are merged and prepended to the inherited - * PATH variable, on the process where a new process is executed. + * when working with variables like {@code PATH}. So to make this work, + * we introduce a special convention {@code PATH+FOO} — all entries + * that starts with {@code PATH+} are merged and prepended to the inherited + * {@code PATH} variable, on the process where a new process is executed. * * @author Kohsuke Kawaguchi */ @@ -84,7 +85,24 @@ public class EnvVars extends TreeMap { * So this property remembers that information. */ private Platform platform; + + /** + * Gets the platform for which these env vars targeted. + * @since 2.144 + * @return The platform. + */ + public @CheckForNull Platform getPlatform() { + return platform; + } + /** + * Sets the platform for which these env vars target. + * @since 2.144 + * @param platform the platform to set. + */ + public void setPlatform(@Nonnull Platform platform) { + this.platform = platform; + } public EnvVars() { super(CaseInsensitiveComparator.INSTANCE); } @@ -107,7 +125,7 @@ public class EnvVars extends TreeMap { } /** - * Builds an environment variables from an array of the form "key","value","key","value"... + * Builds an environment variables from an array of the form {@code "key","value","key","value"...} */ public EnvVars(String... keyValuePairs) { this(); @@ -121,7 +139,7 @@ public class EnvVars extends TreeMap { * Overrides the current entry by the given entry. * *

- * Handles PATH+XYZ notation. + * Handles {@code PATH+XYZ} notation. */ public void override(String key, String value) { if(value==null || value.length()==0) { @@ -425,7 +443,7 @@ public class EnvVars extends TreeMap { * *

* If you access this field from agents, then this is the environment - * variable of the agent agent. + * variable of the agent. */ public static final Map masterEnvVars = initMaster(); diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index 6208f6cec8322a0145f2fc4436519000d07e0ce6..1ff0638c10d6b578927ea90757d5acb8bdbeff6b 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -23,6 +23,7 @@ */ package hudson; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -35,11 +36,13 @@ import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Scope; import com.google.inject.Scopes; +import com.google.inject.matcher.Matchers; import com.google.inject.name.Names; -import com.google.common.collect.ImmutableList; +import com.google.inject.spi.ProvisionListener; import hudson.init.InitMilestone; import hudson.model.Descriptor; import hudson.model.Hudson; +import hudson.util.ReflectionUtils; import jenkins.ExtensionComponentSet; import jenkins.ExtensionFilter; import jenkins.ExtensionRefreshException; @@ -49,20 +52,25 @@ import net.java.sezpoz.Index; import net.java.sezpoz.IndexItem; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.springframework.util.ClassUtils; +import javax.annotation.PostConstruct; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.logging.Logger; +import java.util.Set; import java.util.logging.Level; -import java.util.List; -import java.util.ArrayList; -import java.util.Collection; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Field; -import java.lang.reflect.Method; +import java.util.logging.Logger; /** * Discovers the implementations of an extension point. @@ -254,12 +262,9 @@ public abstract class ExtensionFinder implements ExtensionPoint { private Map,GuiceExtensionAnnotation> extensionAnnotations = Maps.newHashMap(); public GuiceFinder() { - for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) { - GuiceExtensionAnnotation gea = ec.getInstance(); - extensionAnnotations.put(gea.annotationType,gea); - } + refreshExtensionAnnotations(); - sezpozIndex = loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader); + SezpozModule extensions = new SezpozModule(loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader)); List modules = new ArrayList<>(); modules.add(new AbstractModule() { @@ -270,7 +275,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { bind(PluginManager.class).toInstance(j.getPluginManager()); } }); - modules.add(new SezpozModule(sezpozIndex)); + modules.add(extensions); for (ExtensionComponent ec : moduleFinder.find(Module.class, Hudson.getInstance())) { modules.add(ec.getInstance()); @@ -278,6 +283,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { try { container = Guice.createInjector(modules); + sezpozIndex = extensions.getLoadedIndex(); } catch (Throwable e) { LOGGER.log(Level.SEVERE, "Failed to create Guice container from all the plugins",e); // failing to load all bindings are disastrous, so recover by creating minimum that works @@ -293,6 +299,13 @@ public abstract class ExtensionFinder implements ExtensionPoint { }); } + private void refreshExtensionAnnotations() { + for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) { + GuiceExtensionAnnotation gea = ec.getInstance(); + extensionAnnotations.put(gea.annotationType,gea); + } + } + private ImmutableList> loadSezpozIndices(ClassLoader classLoader) { List> indices = Lists.newArrayList(); for (GuiceExtensionAnnotation gea : extensionAnnotations.values()) { @@ -315,17 +328,17 @@ public abstract class ExtensionFinder implements ExtensionPoint { */ @Override public synchronized ExtensionComponentSet refresh() throws ExtensionRefreshException { + refreshExtensionAnnotations(); // figure out newly discovered sezpoz components List> delta = Lists.newArrayList(); for (Class annotationType : extensionAnnotations.keySet()) { delta.addAll(Sezpoz.listDelta(annotationType,sezpozIndex)); } - List> l = Lists.newArrayList(sezpozIndex); - l.addAll(delta); - sezpozIndex = l; + + SezpozModule deltaExtensions = new SezpozModule(delta); List modules = new ArrayList<>(); - modules.add(new SezpozModule(delta)); + modules.add(deltaExtensions); for (ExtensionComponent ec : moduleFinder.refresh().find(Module.class)) { modules.add(ec.getInstance()); } @@ -333,6 +346,9 @@ public abstract class ExtensionFinder implements ExtensionPoint { try { final Injector child = container.createChildInjector(modules); container = child; + List> l = Lists.newArrayList(sezpozIndex); + l.addAll(deltaExtensions.getLoadedIndex()); + sezpozIndex = l; return new ExtensionComponentSet() { @Override @@ -446,11 +462,13 @@ public abstract class ExtensionFinder implements ExtensionPoint { * Instead of using SezPoz to instantiate, we'll instantiate them by using Guice, * so that we can take advantage of dependency injection. */ - private class SezpozModule extends AbstractModule { + private class SezpozModule extends AbstractModule implements ProvisionListener { private final List> index; + private final List> loadedIndex; public SezpozModule(List> index) { this.index = index; + this.loadedIndex = new ArrayList<>(); } /** @@ -493,6 +511,9 @@ public abstract class ExtensionFinder implements ExtensionPoint { @SuppressWarnings({"unchecked", "ChainOfInstanceofChecks"}) @Override protected void configure() { + + bindListener(Matchers.any(), this); + for (final IndexItem item : index) { boolean optional = isOptional(item.annotation()); try { @@ -527,6 +548,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { } }).in(scope); } + loadedIndex.add(item); } catch (Exception|LinkageError e) { // sometimes the instantiation fails in an indirect classloading failure, // which results in a LinkageError @@ -535,9 +557,68 @@ public abstract class ExtensionFinder implements ExtensionPoint { } } } + + public List> getLoadedIndex() { + return Collections.unmodifiableList(loadedIndex); + } + + @Override + public void onProvision(ProvisionInvocation provision) { + final T instance = provision.provision(); + if (instance == null) return; + List methods = new LinkedList<>(); + Class c = instance.getClass(); + + // find PostConstruct methods in class hierarchy, the one from parent class being first in list + // so that we invoke them before derived class one. This isn't specified in JSR-250 but implemented + // this way in Spring and what most developers would expect to happen. + + final Set interfaces = ClassUtils.getAllInterfacesAsSet(instance); + + while (c != Object.class) { + Arrays.stream(c.getDeclaredMethods()) + .map(m -> getMethodAndInterfaceDeclarations(m, interfaces)) + .flatMap(Collection::stream) + .filter(m -> m.getAnnotation(PostConstruct.class) != null) + .findFirst() + .ifPresent(method -> methods.add(0, method)); + c = c.getSuperclass(); + } + + for (Method postConstruct : methods) { + try { + postConstruct.setAccessible(true); + postConstruct.invoke(instance); + } catch (final Exception e) { + throw new RuntimeException(String.format("@PostConstruct %s", postConstruct), e); + } + } + } } } + /** + * Returns initial {@link Method} as well as all matching ones found in interfaces. + * This allows to introspect metadata for a method which is both declared in parent class and in implemented + * interface(s). interfaces typically is obtained by {@link ClassUtils#getAllInterfacesAsSet} + */ + Collection getMethodAndInterfaceDeclarations(Method method, Collection interfaces) { + final List methods = new ArrayList<>(); + methods.add(method); + + // we search for matching method by iteration and comparison vs getMethod to avoid repeated NoSuchMethodException + // being thrown, while interface typically only define a few set of methods to check. + interfaces.stream() + .map(Class::getMethods) + .flatMap(Arrays::stream) + .filter(m -> m.getName().equals(method.getName()) && Arrays.equals(m.getParameterTypes(), method.getParameterTypes())) + .findFirst() + .ifPresent(methods::add); + + return methods; + } + + /** * The bootstrap implementation that looks for the {@link Extension} marker. * diff --git a/core/src/main/java/hudson/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java index f2fcab81d403e63588322b73ceacae33dce66617..566e3de404911db2fbc418bab933a4bdd8261c79 100644 --- a/core/src/main/java/hudson/ExtensionList.java +++ b/core/src/main/java/hudson/ExtensionList.java @@ -30,7 +30,6 @@ import jenkins.ExtensionComponentSet; import jenkins.model.Jenkins; import hudson.util.AdaptedIterator; import hudson.util.DescriptorList; -import hudson.util.Memoizer; import hudson.util.Iterators; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; @@ -40,7 +39,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Vector; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; @@ -144,15 +145,29 @@ public class ExtensionList extends AbstractList implements OnMaster { * Looks for the extension instance of the given type (subclasses excluded), * or return null. */ - public @CheckForNull U get(Class type) { + public @CheckForNull U get(@Nonnull Class type) { for (T ext : this) if(ext.getClass()==type) return type.cast(ext); return null; } + /** + * Looks for the extension instance of the given type (subclasses excluded), + * or throws an IllegalStateException. + * + * Meant to simplify call inside @Extension annotated class to retrieve their own instance. + */ + public @Nonnull U getInstance(@Nonnull Class type) throws IllegalStateException { + for (T ext : this) + if(ext.getClass()==type) + return type.cast(ext); + + throw new IllegalStateException("The class " + type.getName() + " was not found, potentially not yet loaded"); + } + @Override - public Iterator iterator() { + public @Nonnull Iterator iterator() { // we need to intercept mutation, so for now don't allow Iterator.remove return new AdaptedIterator,T>(Iterators.readOnly(ensureLoaded().iterator())) { protected T adapt(ExtensionComponent item) { @@ -395,11 +410,12 @@ public class ExtensionList extends AbstractList implements OnMaster { return create((Jenkins)hudson,type); } + @SuppressWarnings({"unchecked", "rawtypes"}) public static ExtensionList create(Jenkins jenkins, Class type) { if(type.getAnnotation(LegacyInstancesAreScopedToHudson.class)!=null) return new ExtensionList(jenkins,type); else { - return new ExtensionList(jenkins,type,staticLegacyInstances.get(type)); + return new ExtensionList(jenkins, type, staticLegacyInstances.computeIfAbsent(type, key -> new CopyOnWriteArrayList())); } } @@ -418,13 +434,29 @@ public class ExtensionList extends AbstractList implements OnMaster { } /** - * Places to store static-scope legacy instances. + * Convenience method allowing lookup of the only instance of a given type. + * Equivalent to {@code ExtensionList.lookup(Class).get(Class)} if there is one instance, + * and throws an {@code IllegalStateException} otherwise. + * + * @param type The type to look up. + * @return the singleton instance of the given type in its list. + * @throws IllegalStateException if there are no instances, or more than one + * + * @since 2.87 */ - private static final Memoizer staticLegacyInstances = new Memoizer() { - public CopyOnWriteArrayList compute(Class key) { - return new CopyOnWriteArrayList(); + public static @Nonnull U lookupSingleton(Class type) { + ExtensionList all = lookup(type); + if (all.size() != 1) { + throw new IllegalStateException("Expected 1 instance of " + type.getName() + " but got " + all.size()); } - }; + return all.get(0); + } + + /** + * Places to store static-scope legacy instances. + */ + @SuppressWarnings("rawtypes") + private static final Map staticLegacyInstances = new ConcurrentHashMap<>(); /** * Exposed for the test harness to clear all legacy extension instances. diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index cd3fa60451c232c8ce42d1536a16430a08206075..92add6dbe72b396897367f7bd9ea0a2289660e61 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -58,6 +58,8 @@ import hudson.util.IOUtils; import hudson.util.NamingThreadFactory; import hudson.util.io.Archiver; import hudson.util.io.ArchiverFactory; + + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; @@ -77,11 +79,19 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLConnection; +import java.nio.file.FileSystems; import java.nio.file.Files; -import java.nio.file.InvalidPathException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.LinkOption; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.EnumSet; import java.util.Enumeration; import java.util.List; import java.util.Map; @@ -106,12 +116,12 @@ import jenkins.SoloFilePathFilter; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; import jenkins.util.ContextResettingExecutorService; -import jenkins.util.SystemProperties; import jenkins.util.VirtualFile; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.fileupload.FileItem; import org.apache.commons.io.input.CountingInputStream; +import org.apache.commons.lang.StringUtils; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileSet; @@ -124,10 +134,11 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.Stapler; import static hudson.FilePath.TarCompression.GZIP; -import static hudson.Util.deleteFile; +import static hudson.Util.fileToPath; import static hudson.Util.fixEmpty; -import static hudson.Util.isSymlink; + import java.util.Collections; +import org.apache.tools.ant.BuildException; /** * {@link File} like object with remoting support. @@ -214,9 +225,14 @@ public final class FilePath implements Serializable { * This is used to determine whether we are running on the master or the agent. */ private transient VirtualChannel channel; - - // since the platform of the agent might be different, can't use java.io.File - private final String remote; + + /** + * Represent the path to the file in the master or the agent + * Since the platform of the agent might be different, can't use java.io.File + * + * The field could not be final since it's modified in {@link #readResolve()} + */ + private /*final*/ String remote; /** * If this {@link FilePath} is deserialized to handle file access request from a remote computer, @@ -264,6 +280,11 @@ public final class FilePath implements Serializable { this.remote = normalize(resolvePathIfRelative(base, rel)); } + private Object readResolve() { + this.remote = normalize(this.remote); + return this; + } + private String resolvePathIfRelative(@Nonnull FilePath base, @Nonnull String rel) { if(isAbsolute(rel)) return rel; if(base.isUnix()) { @@ -291,7 +312,8 @@ public final class FilePath implements Serializable { * {@link File#getParent()} etc cannot handle ".." and "." in the path component very well, * so remove them. */ - private static String normalize(@Nonnull String path) { + @Restricted(NoExternalUse.class) + public static String normalize(@Nonnull String path) { StringBuilder buf = new StringBuilder(); // Check for prefix designating absolute path Matcher m = ABSOLUTE_PREFIX_PATTERN.matcher(path); @@ -455,7 +477,18 @@ public final class FilePath implements Serializable { */ public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException { final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os; - return act(new SecureFileCallable() { + return act(new Archive(factory, out, scanner)); + } + private class Archive extends SecureFileCallable { + private final ArchiverFactory factory; + private final OutputStream out; + private final DirScanner scanner; + Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner) { + this.factory = factory; + this.out = out; + this.scanner = scanner; + } + @Override public Integer invoke(File f, VirtualChannel channel) throws IOException { Archiver a = factory.create(out); try { @@ -467,7 +500,6 @@ public final class FilePath implements Serializable { } private static final long serialVersionUID = 1L; - }); } public int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException { @@ -490,27 +522,32 @@ public final class FilePath implements Serializable { // TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream if (this.channel!=target.channel) {// local -> remote or remote->local final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY); - target.act(new SecureFileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - unzip(dir, in); - return null; - } - - private static final long serialVersionUID = 1L; - }); + target.act(new UnzipRemote(in)); } else {// local -> local or remote->remote - target.act(new SecureFileCallable() { - - public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - assert !FilePath.this.isRemote(); // this.channel==target.channel above - unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file - return null; - } - - private static final long serialVersionUID = 1L; - }); + target.act(new UnzipLocal()); } } + private class UnzipRemote extends SecureFileCallable { + private final RemoteInputStream in; + UnzipRemote(RemoteInputStream in) { + this.in = in; + } + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + unzip(dir, in); + return null; + } + private static final long serialVersionUID = 1L; + } + private class UnzipLocal extends SecureFileCallable { + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + assert !FilePath.this.isRemote(); // this.channel==target.channel above + unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file + return null; + } + private static final long serialVersionUID = 1L; + } /** * When this {@link FilePath} represents a tar file, extracts that tar file. @@ -526,23 +563,36 @@ public final class FilePath implements Serializable { // TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream if (this.channel!=target.channel) {// local -> remote or remote->local final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY); - target.act(new SecureFileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - readFromTar(FilePath.this.getName(),dir,compression.extract(in)); - return null; - } - - private static final long serialVersionUID = 1L; - }); + target.act(new UntarRemote(compression, in)); } else {// local -> local or remote->remote - target.act(new SecureFileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { - readFromTar(FilePath.this.getName(),dir,compression.extract(FilePath.this.read())); - return null; - } - private static final long serialVersionUID = 1L; - }); + target.act(new UntarLocal(compression)); + } + } + private class UntarRemote extends SecureFileCallable { + private final TarCompression compression; + private final RemoteInputStream in; + UntarRemote(TarCompression compression, RemoteInputStream in) { + this.compression = compression; + this.in = in; + } + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + readFromTar(FilePath.this.getName(), dir, compression.extract(in)); + return null; + } + private static final long serialVersionUID = 1L; + } + private class UntarLocal extends SecureFileCallable { + private final TarCompression compression; + UntarLocal(TarCompression compression) { + this.compression = compression; + } + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + readFromTar(FilePath.this.getName(), dir, compression.extract(FilePath.this.read())); + return null; } + private static final long serialVersionUID = 1L; } /** @@ -555,13 +605,19 @@ public final class FilePath implements Serializable { */ public void unzipFrom(InputStream _in) throws IOException, InterruptedException { final InputStream in = new RemoteInputStream(_in, Flag.GREEDY); - act(new SecureFileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException { - unzip(dir, in); - return null; - } - private static final long serialVersionUID = 1L; - }); + act(new UnzipFrom(in)); + } + private class UnzipFrom extends SecureFileCallable { + private final InputStream in; + UnzipFrom(InputStream in) { + this.in = in; + } + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException { + unzip(dir, in); + return null; + } + private static final long serialVersionUID = 1L; } private void unzip(File dir, InputStream in) throws IOException { @@ -586,6 +642,10 @@ public final class FilePath implements Serializable { while (entries.hasMoreElements()) { ZipEntry e = entries.nextElement(); File f = new File(dir, e.getName()); + if (!f.toPath().normalize().startsWith(dir.toPath())) { + throw new IOException( + "Zip " + zipFile.getPath() + " contains illegal file name that breaks out of the target directory: " + e.getName()); + } if (e.isDirectory()) { mkdirs(f); } else { @@ -616,12 +676,13 @@ public final class FilePath implements Serializable { * Absolutizes this {@link FilePath} and returns the new one. */ public FilePath absolutize() throws IOException, InterruptedException { - return new FilePath(channel,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File f, VirtualChannel channel) throws IOException { - return f.getAbsolutePath(); - } - })); + return new FilePath(channel, act(new Absolutize())); + } + private static class Absolutize extends SecureFileCallable { + private static final long serialVersionUID = 1L; + public String invoke(File f, VirtualChannel channel) throws IOException { + return f.getAbsolutePath(); + } } /** @@ -634,14 +695,22 @@ public final class FilePath implements Serializable { * @since 1.456 */ public void symlinkTo(final String target, final TaskListener listener) throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - symlinking(f); - Util.createSymlink(f.getParentFile(),target,f.getName(),listener); - return null; - } - }); + act(new SymlinkTo(target, listener)); + } + private class SymlinkTo extends SecureFileCallable { + private final String target; + private final TaskListener listener; + SymlinkTo(String target, TaskListener listener) { + this.target = target; + this.listener = listener; + } + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + symlinking(f); + Util.createSymlink(f.getParentFile(), target, f.getName(), listener); + return null; + } } /** @@ -652,12 +721,14 @@ public final class FilePath implements Serializable { * @since 1.456 */ public String readLink() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return Util.resolveSymlink(reading(f)); - } - }); + return act(new ReadLink()); + } + private class ReadLink extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return Util.resolveSymlink(reading(f)); + } } @Override @@ -721,17 +792,25 @@ public final class FilePath implements Serializable { public void untarFrom(InputStream _in, final TarCompression compression) throws IOException, InterruptedException { try { final InputStream in = new RemoteInputStream(_in, Flag.GREEDY); - act(new SecureFileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException { - readFromTar("input stream",dir, compression.extract(in)); - return null; - } - private static final long serialVersionUID = 1L; - }); + act(new UntarFrom(compression, in)); } finally { _in.close(); } } + private class UntarFrom extends SecureFileCallable { + private final TarCompression compression; + private final InputStream in; + UntarFrom(TarCompression compression, InputStream in) { + this.compression = compression; + this.in = in; + } + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException { + readFromTar("input stream",dir, compression.extract(in)); + return null; + } + private static final long serialVersionUID = 1L; + } /** * Given a tgz/zip file, extracts it to the given target directory, if necessary. @@ -881,9 +960,10 @@ public final class FilePath implements Serializable { } /** - * Reads the URL on the current VM, and writes all the data to this {@link FilePath} - * (this is different from resolving URL remotely.) - * + * Reads the URL on the current VM, and streams the data to this file using the Remoting channel. + *

This is different from resolving URL remotely. + * If you instead wished to open an HTTP(S) URL on the remote side, + * prefer {@code RobustHTTPClient.copyFromRemotely}. * @since 1.293 */ public void copyFrom(URL url) throws IOException, InterruptedException { @@ -987,11 +1067,6 @@ public final class FilePath implements Serializable { return channel.call(wrapper); } catch (TunneledInterruptedException e) { throw (InterruptedException)new InterruptedException(e.getMessage()).initCause(e); - } catch (AbortException e) { - throw e; // pass through so that the caller can catch it as AbortException - } catch (IOException e) { - // wrap it into a new IOException so that we get the caller's stack trace as well. - throw new IOException("remote file operation failed: " + remote + " at " + channel + ": " + e, e); } } else { // the file is on the local machine. @@ -1094,23 +1169,28 @@ public final class FilePath implements Serializable { * @since 1.522 */ public Callable asCallableWith(final FileCallable task) { - return new Callable() { - @Override - public V call() throws IOException { - try { - return act(task); - } catch (InterruptedException e) { - throw (IOException)new InterruptedIOException().initCause(e); - } + return new CallableWith<>(task); + } + private class CallableWith implements Callable { + private final FileCallable task; + CallableWith(FileCallable task) { + this.task = task; + } + @Override + public V call() throws IOException { + try { + return act(task); + } catch (InterruptedException e) { + throw (IOException)new InterruptedIOException().initCause(e); } + } - @Override - public void checkRoles(RoleChecker checker) throws SecurityException { - task.checkRoles(checker); - } + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + task.checkRoles(checker); + } - private static final long serialVersionUID = 1L; - }; + private static final long serialVersionUID = 1L; } /** @@ -1118,12 +1198,14 @@ public final class FilePath implements Serializable { * on which this file is available. */ public URI toURI() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public URI invoke(File f, VirtualChannel channel) { - return f.toURI(); - } - }); + return act(new ToURI()); + } + private static class ToURI extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public URI invoke(File f, VirtualChannel channel) { + return f.toURI(); + } } /** @@ -1156,70 +1238,54 @@ public final class FilePath implements Serializable { * Creates this directory. */ public void mkdirs() throws IOException, InterruptedException { - if(!act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - if(mkdirs(f) || f.exists()) - return true; // OK + if (!act(new Mkdirs())) { + throw new IOException("Failed to mkdirs: " + remote); + } + } + private class Mkdirs extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + if(mkdirs(f) || f.exists()) + return true; // OK - // following Ant task to avoid possible race condition. - Thread.sleep(10); + // following Ant task to avoid possible race condition. + Thread.sleep(10); - return f.mkdirs() || f.exists(); - } - })) - throw new IOException("Failed to mkdirs: "+remote); + return mkdirs(f) || f.exists(); + } } /** * Deletes this directory, including all its contents recursively. */ public void deleteRecursive() throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - deleteRecursive(deleting(f)); - return null; - } - }); + act(new DeleteRecursive()); + } + private class DeleteRecursive extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Util.deleteRecursive(fileToPath(f), path -> deleting(path.toFile())); + return null; + } } /** * Deletes all the contents of this directory, but not the directory itself */ public void deleteContents() throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - deleteContentsRecursive(f); - return null; - } - }); + act(new DeleteContents()); } - - private void deleteRecursive(File dir) throws IOException { - if(!isSymlink(dir)) - deleteContentsRecursive(dir); - try { - deleteFile(deleting(dir)); - } catch (IOException e) { - // if some of the child directories are big, it might take long enough to delete that - // it allows others to create new files, causing problems like JENKINS-10113 - // so give it one more attempt before we give up. - if(!isSymlink(dir)) - deleteContentsRecursive(dir); - deleteFile(deleting(dir)); + private class DeleteContents extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Util.deleteContentsRecursive(fileToPath(f), path -> deleting(path.toFile())); + return null; } } - private void deleteContentsRecursive(File file) throws IOException { - File[] files = file.listFiles(); - if(files==null) - return; // the directory didn't exist in the first place - for (File child : files) - deleteRecursive(child); - } - /** * Gets the file name portion except the extension. * @@ -1305,17 +1371,25 @@ public final class FilePath implements Serializable { */ public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File dir, VirtualChannel channel) throws IOException { - File f = writing(File.createTempFile(prefix, suffix, dir)); - return f.getName(); - } - })); + return new FilePath(this, act(new CreateTempFile(prefix, suffix))); } catch (IOException e) { throw new IOException("Failed to create a temp file on "+remote,e); } } + private class CreateTempFile extends SecureFileCallable { + private final String prefix; + private final String suffix; + CreateTempFile(String prefix, String suffix) { + this.prefix = prefix; + this.suffix = suffix; + } + private static final long serialVersionUID = 1L; + @Override + public String invoke(File dir, VirtualChannel channel) throws IOException { + File f = writing(File.createTempFile(prefix, suffix, dir)); + return f.getName(); + } + } /** * Creates a temporary file in this directory and set the contents to the @@ -1361,30 +1435,42 @@ public final class FilePath implements Serializable { */ public FilePath createTextTempFile(final String prefix, final String suffix, final String contents, final boolean inThisDirectory) throws IOException, InterruptedException { try { - return new FilePath(channel,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File dir, VirtualChannel channel) throws IOException { - if(!inThisDirectory) - dir = new File(System.getProperty("java.io.tmpdir")); - else - mkdirs(dir); + return new FilePath(channel, act(new CreateTextTempFile(inThisDirectory, prefix, suffix, contents))); + } catch (IOException e) { + throw new IOException("Failed to create a temp file on "+remote,e); + } + } + private final class CreateTextTempFile extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final boolean inThisDirectory; + private final String prefix; + private final String suffix; + private final String contents; + CreateTextTempFile(boolean inThisDirectory, String prefix, String suffix, String contents) { + this.inThisDirectory = inThisDirectory; + this.prefix = prefix; + this.suffix = suffix; + this.contents = contents; + } + @Override + public String invoke(File dir, VirtualChannel channel) throws IOException { + if(!inThisDirectory) + dir = new File(System.getProperty("java.io.tmpdir")); + else + mkdirs(dir); - File f; - try { - f = creating(File.createTempFile(prefix, suffix, dir)); - } catch (IOException e) { - throw new IOException("Failed to create a temporary directory in "+dir,e); - } + File f; + try { + f = creating(File.createTempFile(prefix, suffix, dir)); + } catch (IOException e) { + throw new IOException("Failed to create a temporary directory in "+dir,e); + } - try (Writer w = new FileWriter(writing(f))) { - w.write(contents); - } + try (Writer w = new FileWriter(writing(f))) { + w.write(contents); + } - return f.getAbsolutePath(); - } - })); - } catch (IOException e) { - throw new IOException("Failed to create a temp file on "+remote,e); + return f.getAbsolutePath(); } } @@ -1400,23 +1486,47 @@ public final class FilePath implements Serializable { * @return * The new FilePath pointing to the temporary directory * @since 1.311 - * @see File#createTempFile(String, String) + * @see Files#createTempDirectory(Path, String, FileAttribute[]) */ public FilePath createTempDir(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File dir, VirtualChannel channel) throws IOException { - File f = File.createTempFile(prefix, suffix, dir); - f.delete(); - f.mkdir(); - return f.getName(); - } - })); + String[] s; + if (StringUtils.isBlank(suffix)) { + s = new String[]{prefix, "tmp"}; // see File.createTempFile - tmp is used if suffix is null + } else { + s = new String[]{prefix, suffix}; + } + String name = StringUtils.join(s, "."); + return new FilePath(this, act(new CreateTempDir(name))); } catch (IOException e) { throw new IOException("Failed to create a temp directory on "+remote,e); } } + private class CreateTempDir extends SecureFileCallable { + private final String name; + CreateTempDir(String name) { + this.name = name; + } + private static final long serialVersionUID = 1L; + @Override + public String invoke(File dir, VirtualChannel channel) throws IOException { + + Path tempPath; + final boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + + if (isPosix) { + tempPath = Files.createTempDirectory(Util.fileToPath(dir), name, + PosixFilePermissions.asFileAttribute(EnumSet.allOf(PosixFilePermission.class))); + } else { + tempPath = Files.createTempDirectory(Util.fileToPath(dir), name, new FileAttribute[] {}); + } + + if (tempPath.toFile() == null) { + throw new IOException("Failed to obtain file from path " + dir + " on " + remote); + } + return tempPath.toFile().getName(); + } + } /** * Deletes this file. @@ -1424,26 +1534,30 @@ public final class FilePath implements Serializable { * @return true, for a modicum of compatibility */ public boolean delete() throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteFile(deleting(f)); - return null; - } - }); + act(new Delete()); return true; } + private class Delete extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Util.deleteFile(deleting(f)); + return null; + } + } /** * Checks if the file exists. */ public boolean exists() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).exists(); - } - }); + return act(new Exists()); + } + private class Exists extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).exists(); + } } /** @@ -1454,12 +1568,14 @@ public final class FilePath implements Serializable { * @see #touch(long) */ public long lastModified() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Long invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).lastModified(); - } - }); + return act(new LastModified()); + } + private class LastModified extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).lastModified(); + } } /** @@ -1468,26 +1584,39 @@ public final class FilePath implements Serializable { * @since 1.299 */ public void touch(final long timestamp) throws IOException, InterruptedException { - act(new SecureFileCallable() { + act(new Touch(timestamp)); + } + private class Touch extends SecureFileCallable { + private final long timestamp; + Touch(long timestamp) { + this.timestamp = timestamp; + } private static final long serialVersionUID = -5094638816500738429L; + @Override public Void invoke(File f, VirtualChannel channel) throws IOException { if(!f.exists()) { - try { - Files.newOutputStream(creating(f).toPath()).close(); - } catch (InvalidPathException e) { - throw new IOException(e); - } + Files.newOutputStream(fileToPath(creating(f))).close(); } if(!stating(f).setLastModified(timestamp)) throw new IOException("Failed to set the timestamp of "+f+" to "+timestamp); return null; } - }); } private void setLastModifiedIfPossible(final long timestamp) throws IOException, InterruptedException { - String message = act(new SecureFileCallable() { + String message = act(new SetLastModified(timestamp)); + + if (message!=null) { + LOGGER.warning(message); + } + } + private class SetLastModified extends SecureFileCallable { + private final long timestamp; + SetLastModified(long timestamp) { + this.timestamp = timestamp; + } private static final long serialVersionUID = -828220335793641630L; + @Override public String invoke(File f, VirtualChannel channel) throws IOException { if(!writing(f).setLastModified(timestamp)) { if (Functions.isWindows()) { @@ -1500,23 +1629,20 @@ public final class FilePath implements Serializable { } return null; } - }); - - if (message!=null) { - LOGGER.warning(message); - } } /** * Checks if the file is a directory. */ public boolean isDirectory() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).isDirectory(); - } - }); + return act(new IsDirectory()); + } + private final class IsDirectory extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).isDirectory(); + } } /** @@ -1525,12 +1651,14 @@ public final class FilePath implements Serializable { * @since 1.129 */ public long length() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Long invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).length(); - } - }); + return act(new Length()); + } + private class Length extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).length(); + } } /** @@ -1538,12 +1666,14 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getFreeDiskSpace() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.getFreeSpace(); - } - }); + return act(new GetFreeDiskSpace()); + } + private static class GetFreeDiskSpace extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getFreeSpace(); + } } /** @@ -1551,12 +1681,14 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getTotalDiskSpace() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.getTotalSpace(); - } - }); + return act(new GetTotalDiskSpace()); + } + private static class GetTotalDiskSpace extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getTotalSpace(); + } } /** @@ -1564,12 +1696,14 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getUsableDiskSpace() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.getUsableSpace(); - } - }); + return act(new GetUsableDiskSpace()); + } + private static class GetUsableDiskSpace extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getUsableSpace(); + } } /** @@ -1583,32 +1717,46 @@ public final class FilePath implements Serializable { *

* please note mask is expected to be an octal if you use chmod command line values, * so preceded by a '0' in java notation, ie chmod(0644) + *

+ * Only supports setting read, write, or execute permissions for the + * owner, group, or others, so the largest permissible value is 0777. + * Attempting to set larger values (i.e. the setgid, setuid, or sticky + * bits) will cause an IOException to be thrown. * * @since 1.303 * @see #mode() */ public void chmod(final int mask) throws IOException, InterruptedException { if(!isUnix() || mask==-1) return; - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - // TODO first check for Java 7+ and use PosixFileAttributeView - _chmod(writing(f), mask); + act(new Chmod(mask)); + } + private class Chmod extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final int mask; + Chmod(int mask) { + this.mask = mask; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + _chmod(writing(f), mask); - return null; - } - }); + return null; + } } /** - * Run chmod via jnr-posix + * Change permissions via NIO. */ private static void _chmod(File f, int mask) throws IOException { // TODO WindowsPosix actually does something here (WindowsLibC._wchmod); should we let it? // Anyway the existing calls already skip this method if on Windows. if (File.pathSeparatorChar==';') return; // noop - PosixAPI.jnr().chmod(f.getAbsolutePath(),mask); + if (Util.NATIVE_CHMOD_MODE) { + PosixAPI.jnr().chmod(f.getAbsolutePath(), mask); + } else { + Files.setPosixFilePermissions(fileToPath(f), Util.modeToPermissions(mask)); + } } private static boolean CHMOD_WARNED = false; @@ -1623,12 +1771,14 @@ public final class FilePath implements Serializable { */ public int mode() throws IOException, InterruptedException, PosixException { if(!isUnix()) return -1; - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Integer invoke(File f, VirtualChannel channel) throws IOException { - return IOUtils.mode(stating(f)); - } - }); + return act(new Mode()); + } + private class Mode extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Integer invoke(File f, VirtualChannel channel) throws IOException { + return IOUtils.mode(stating(f)); + } } /** @@ -1673,8 +1823,15 @@ public final class FilePath implements Serializable { if (filter != null && !(filter instanceof Serializable)) { throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass()); } - return act(new SecureFileCallable>() { + return act(new ListFilter(filter), (filter != null ? filter : this).getClass().getClassLoader()); + } + private class ListFilter extends SecureFileCallable> { + private final FileFilter filter; + ListFilter(FileFilter filter) { + this.filter = filter; + } private static final long serialVersionUID = 1L; + @Override public List invoke(File f, VirtualChannel channel) throws IOException { File[] children = reading(f).listFiles(filter); if (children == null) { @@ -1687,7 +1844,6 @@ public final class FilePath implements Serializable { return r; } - }, (filter!=null?filter:this).getClass().getClassLoader()); } /** @@ -1731,8 +1887,19 @@ public final class FilePath implements Serializable { */ @Nonnull public FilePath[] list(final String includes, final String excludes, final boolean defaultExcludes) throws IOException, InterruptedException { - return act(new SecureFileCallable() { + return act(new ListGlob(includes, excludes, defaultExcludes)); + } + private class ListGlob extends SecureFileCallable { + private final String includes; + private final String excludes; + private final boolean defaultExcludes; + ListGlob(String includes, String excludes, boolean defaultExcludes) { + this.includes = includes; + this.excludes = excludes; + this.defaultExcludes = defaultExcludes; + } private static final long serialVersionUID = 1L; + @Override public FilePath[] invoke(File f, VirtualChannel channel) throws IOException { String[] files = glob(reading(f), includes, excludes, defaultExcludes); @@ -1742,7 +1909,6 @@ public final class FilePath implements Serializable { return r; } - }); } /** @@ -1757,7 +1923,12 @@ public final class FilePath implements Serializable { throw new IOException("Expecting Ant GLOB pattern, but saw '"+includes+"'. See http://ant.apache.org/manual/Types/fileset.html for syntax"); FileSet fs = Util.createFileSet(dir,includes,excludes); fs.setDefaultexcludes(defaultExcludes); - DirectoryScanner ds = fs.getDirectoryScanner(new Project()); + DirectoryScanner ds; + try { + ds = fs.getDirectoryScanner(new Project()); + } catch (BuildException x) { + throw new IOException(x.getMessage()); + } String[] files = ds.getIncludedFiles(); return files; } @@ -1767,33 +1938,31 @@ public final class FilePath implements Serializable { */ public InputStream read() throws IOException, InterruptedException { if(channel==null) { - try { - return Files.newInputStream(reading(new File(remote)).toPath()); - } catch (InvalidPathException e) { - throw new IOException(e); - } + return Files.newInputStream(fileToPath(reading(new File(remote)))); } final Pipe p = Pipe.createRemoteToLocal(); - actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - - @Override - public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - try (InputStream fis = Files.newInputStream(reading(f).toPath()); - OutputStream out = p.getOut()) { - org.apache.commons.io.IOUtils.copy(fis, out); - } catch (InvalidPathException e) { - p.error(new IOException(e)); - } catch (Exception x) { - p.error(x); - } - return null; - } - }); + actAsync(new Read(p)); return p.getIn(); } + private class Read extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final Pipe p; + Read(Pipe p) { + this.p = p; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + try (InputStream fis = Files.newInputStream(fileToPath(reading(f))); + OutputStream out = p.getOut()) { + org.apache.commons.io.IOUtils.copy(fis, out); + } catch (Exception x) { + p.error(x); + } + return null; + } + } /** * Reads this file from the specific offset. @@ -1858,11 +2027,16 @@ public final class FilePath implements Serializable { } /** - * Reads this file into a string, by using the current system encoding. + * Reads this file into a string, by using the current system encoding on the remote machine. */ public String readToString() throws IOException, InterruptedException { - try (InputStream in = read()) { - return org.apache.commons.io.IOUtils.toString(in); + return act(new ReadToString()); + } + private final class ReadToString extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return new String(Files.readAllBytes(fileToPath(reading(f)))); } } @@ -1885,49 +2059,48 @@ public final class FilePath implements Serializable { if(channel==null) { File f = new File(remote).getAbsoluteFile(); mkdirs(f.getParentFile()); - try { - return Files.newOutputStream(writing(f).toPath()); - } catch (InvalidPathException e) { - throw new IOException(e); - } + return Files.newOutputStream(fileToPath(writing(f))); } - return act(new SecureFileCallable() { + return act(new WritePipe()); + } + private class WritePipe extends SecureFileCallable { private static final long serialVersionUID = 1L; + @Override public OutputStream invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { f = f.getAbsoluteFile(); mkdirs(f.getParentFile()); - try { - OutputStream fos = Files.newOutputStream(writing(f).toPath()); - return new RemoteOutputStream(fos); - } catch (InvalidPathException e) { - throw new IOException(e); - } + return new RemoteOutputStream(Files.newOutputStream(fileToPath(writing(f)))); } - }); } /** * Overwrites this file by placing the given String as the content. * * @param encoding - * Null to use the platform default encoding. + * Null to use the platform default encoding on the remote machine. * @since 1.105 */ public void write(final String content, final String encoding) throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - mkdirs(f.getParentFile()); - try (OutputStream fos = Files.newOutputStream(writing(f).toPath()); - Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos)) { - w.write(content); - } catch (InvalidPathException e) { - throw new IOException(e); - } - return null; + act(new Write(encoding, content)); + } + private class Write extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final String encoding; + private final String content; + Write(String encoding, String content) { + this.encoding = encoding; + this.content = content; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + mkdirs(f.getParentFile()); + try (OutputStream fos = Files.newOutputStream(fileToPath(writing(f))); + Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos)) { + w.write(content); } - }); + return null; + } } /** @@ -1935,12 +2108,14 @@ public final class FilePath implements Serializable { * @see Util#getDigestOf(File) */ public String digest() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File f, VirtualChannel channel) throws IOException { - return Util.getDigestOf(reading(f)); - } - }); + return act(new Digest()); + } + private class Digest extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public String invoke(File f, VirtualChannel channel) throws IOException { + return Util.getDigestOf(reading(f)); + } } /** @@ -1951,13 +2126,19 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("renameTo target must be on the same host"); } - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - reading(f).renameTo(creating(new File(target.remote))); - return null; - } - }); + act(new RenameTo(target)); + } + private class RenameTo extends SecureFileCallable { + private final FilePath target; + RenameTo(FilePath target) { + this.target = target; + } + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Files.move(fileToPath(reading(f)), fileToPath(creating(new File(target.remote))), LinkOption.NOFOLLOW_LINKS); + return null; + } } /** @@ -1969,8 +2150,15 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("pullUpTo target must be on the same host"); } - act(new SecureFileCallable() { + act(new MoveAllChildrenTo(target)); + } + private class MoveAllChildrenTo extends SecureFileCallable { + private final FilePath target; + MoveAllChildrenTo(FilePath target) { + this.target = target; + } private static final long serialVersionUID = 1L; + @Override public Void invoke(File f, VirtualChannel channel) throws IOException { // JENKINS-16846: if f.getName() is the same as one of the files/directories in f, // then the rename op will fail @@ -1979,7 +2167,7 @@ public final class FilePath implements Serializable { throw new IOException("Failed to rename "+f+" to "+tmp); File t = new File(target.getRemote()); - + for(File child : reading(tmp).listFiles()) { File target = new File(t, child.getName()); if(!stating(child).renameTo(creating(target))) @@ -1988,7 +2176,6 @@ public final class FilePath implements Serializable { deleting(tmp).delete(); return null; } - }); } /** @@ -2009,11 +2196,32 @@ public final class FilePath implements Serializable { * @since 1.311 */ public void copyToWithPermission(FilePath target) throws IOException, InterruptedException { + // Use NIO copy with StandardCopyOption.COPY_ATTRIBUTES when copying on the same machine. + if (this.channel == target.channel) { + act(new CopyToWithPermission(target)); + return; + } + copyTo(target); // copy file permission target.chmod(mode()); target.setLastModifiedIfPossible(lastModified()); } + private class CopyToWithPermission extends SecureFileCallable { + private final FilePath target; + CopyToWithPermission(FilePath target) { + this.target = target; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + File targetFile = new File(target.remote); + File targetDir = targetFile.getParentFile(); + filterNonNull().mkdirs(targetDir); + Files.createDirectories(fileToPath(targetDir)); + Files.copy(fileToPath(reading(f)), fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + return null; + } + } /** * Sends the contents of this file into the given {@link OutputStream}. @@ -2021,28 +2229,32 @@ public final class FilePath implements Serializable { public void copyTo(OutputStream os) throws IOException, InterruptedException { final OutputStream out = new RemoteOutputStream(os); - act(new SecureFileCallable() { - private static final long serialVersionUID = 4088559042349254141L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - try (InputStream fis = Files.newInputStream(reading(f).toPath())) { - org.apache.commons.io.IOUtils.copy(fis, out); - return null; - } catch (InvalidPathException e) { - throw new IOException(e); - } finally { - out.close(); - } - } - }); + act(new CopyTo(out)); // make sure the writes fully got delivered to 'os' before we return. // this is needed because I/O operation is asynchronous syncIO(); } + private class CopyTo extends SecureFileCallable { + private static final long serialVersionUID = 4088559042349254141L; + private final OutputStream out; + CopyTo(OutputStream out) { + this.out = out; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + try (InputStream fis = Files.newInputStream(fileToPath(reading(f)))) { + org.apache.commons.io.IOUtils.copy(fis, out); + return null; + } finally { + out.close(); + } + } + } /** * With fix to JENKINS-11251 (remoting 2.15), this is no longer necessary. - * But I'm keeping it for a while so that users who manually deploy slave.jar has time to deploy new version + * But I'm keeping it for a while so that users who manually deploy agent.jar has time to deploy new version * before this goes away. */ private void syncIO() throws InterruptedException { @@ -2050,10 +2262,10 @@ public final class FilePath implements Serializable { if (channel!=null) channel.syncLocalIO(); } catch (AbstractMethodError e) { - // legacy slave.jar. Handle this gracefully + // legacy agent.jar. Handle this gracefully try { - LOGGER.log(Level.WARNING,"Looks like an old slave.jar. Please update "+ Which.jarFile(Channel.class)+" to the new version",e); - } catch (IOException _) { + LOGGER.log(Level.WARNING,"Looks like an old agent.jar. Please update "+ Which.jarFile(Channel.class)+" to the new version",e); + } catch (IOException ignored) { // really ignore this time } } @@ -2136,83 +2348,31 @@ public final class FilePath implements Serializable { public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException { if(this.channel==target.channel) { // local to local copy. - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Integer invoke(File base, VirtualChannel channel) throws IOException { - if(!base.exists()) return 0; - assert target.channel==null; - final File dest = new File(target.remote); - final AtomicInteger count = new AtomicInteger(); - scanner.scan(base, reading(new FileVisitor() { - @Override - public void visit(File f, String relativePath) throws IOException { - if (f.isFile()) { - File target = new File(dest, relativePath); - mkdirsE(target.getParentFile()); - Util.copyFile(f, writing(target)); - count.incrementAndGet(); - } - } - - @Override - public boolean understandsSymlink() { - return true; - } - - @Override - public void visitSymlink(File link, String target, String relativePath) throws IOException { - try { - mkdirsE(new File(dest, relativePath).getParentFile()); - writing(new File(dest, target)); - Util.createSymlink(dest, target, relativePath, TaskListener.NULL); - } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); - } - count.incrementAndGet(); - } - })); - return count.get(); - } - }); + return act(new CopyRecursiveLocal(target, scanner)); } else if(this.channel==null) { // local -> remote copy final Pipe pipe = Pipe.createLocalToRemote(); - Future future = target.actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - try (InputStream in = pipe.getIn()) { - readFromTar(remote + '/' + description, f,TarCompression.GZIP.extract(in)); - return null; - } - } - }); - Future future2 = actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); - } - }); + Future future = target.actAsync(new ReadToTar(pipe, description)); + Future future2 = actAsync(new WriteToTar(scanner, pipe)); try { // JENKINS-9540 in case the reading side failed, report that error first future.get(); return future2.get(); } catch (ExecutionException e) { - throw new IOException(e); + Throwable cause = e.getCause(); + if (cause == null) cause = e; + throw cause instanceof IOException + ? (IOException) cause + : new IOException(cause) + ; } } else { // remote -> local copy final Pipe pipe = Pipe.createRemoteToLocal(); - Future future = actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Integer invoke(File f, VirtualChannel channel) throws IOException { - try (OutputStream out = pipe.getOut()) { - return writeToTar(f, scanner, TarCompression.GZIP.compress(out)); - } - } - }); + Future future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner)); try { readFromTar(remote + '/' + description,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn())); } catch (IOException e) {// BuildException or IOException @@ -2221,20 +2381,136 @@ public final class FilePath implements Serializable { throw e; // the remote side completed successfully, so the error must be local } catch (ExecutionException x) { // report both errors - throw new IOException(Functions.printThrowable(e),x); - } catch (TimeoutException _) { - // remote is hanging + e.addSuppressed(x); + throw e; + } catch (TimeoutException ignored) { + // remote is hanging, just throw the original exception throw e; } } try { return future.get(); } catch (ExecutionException e) { - throw new IOException(e); + Throwable cause = e.getCause(); + if (cause == null) cause = e; + throw cause instanceof IOException + ? (IOException) cause + : new IOException(cause) + ; + } + } + } + private class CopyRecursiveLocal extends SecureFileCallable { + private final FilePath target; + private final DirScanner scanner; + CopyRecursiveLocal(FilePath target, DirScanner scanner) { + this.target = target; + this.scanner = scanner; + } + private static final long serialVersionUID = 1L; + @Override + public Integer invoke(File base, VirtualChannel channel) throws IOException { + if (!base.exists()) { + return 0; + } + assert target.channel == null; + final File dest = new File(target.remote); + final AtomicInteger count = new AtomicInteger(); + scanner.scan(base, reading(new FileVisitor() { + private boolean exceptionEncountered; + private boolean logMessageShown; + @Override + public void visit(File f, String relativePath) throws IOException { + if (f.isFile()) { + File target = new File(dest, relativePath); + mkdirsE(target.getParentFile()); + Path targetPath = fileToPath(writing(target)); + exceptionEncountered = exceptionEncountered || !tryCopyWithAttributes(f, targetPath); + if (exceptionEncountered) { + Files.copy(fileToPath(f), targetPath, StandardCopyOption.REPLACE_EXISTING); + if (!logMessageShown) { + LOGGER.log(Level.INFO, + "JENKINS-52325: Jenkins failed to retain attributes when copying to {0}, so proceeding without attributes.", + dest.getAbsolutePath()); + logMessageShown = true; + } + } + count.incrementAndGet(); + } + } + private boolean tryCopyWithAttributes(File f, Path targetPath) { + try { + Files.copy(fileToPath(f), targetPath, + StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Unable to copy: {0}", e.getMessage()); + return false; + } + return true; + } + @Override + public boolean understandsSymlink() { + return true; + } + @Override + public void visitSymlink(File link, String target, String relativePath) throws IOException { + try { + mkdirsE(new File(dest, relativePath).getParentFile()); + writing(new File(dest, target)); + Util.createSymlink(dest, target, relativePath, TaskListener.NULL); + } catch (InterruptedException x) { + throw new IOException(x); + } + count.incrementAndGet(); + } + })); + return count.get(); + } + } + private class ReadToTar extends SecureFileCallable { + private final Pipe pipe; + private final String description; + ReadToTar(Pipe pipe, String description) { + this.pipe = pipe; + this.description = description; + } + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + try (InputStream in = pipe.getIn()) { + readFromTar(remote + '/' + description, f, TarCompression.GZIP.extract(in)); + return null; + } + } + } + private class WriteToTar extends SecureFileCallable { + private final DirScanner scanner; + private final Pipe pipe; + WriteToTar(DirScanner scanner, Pipe pipe) { + this.scanner = scanner; + this.pipe = pipe; + } + private static final long serialVersionUID = 1L; + @Override + public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); + } + } + private class CopyRecursiveRemoteToLocal extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final Pipe pipe; + private final DirScanner scanner; + CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner) { + this.pipe = pipe; + this.scanner = scanner; + } + @Override + public Integer invoke(File f, VirtualChannel channel) throws IOException { + try (OutputStream out = pipe.getOut()) { + return writeToTar(f, scanner, TarCompression.GZIP.compress(out)); } } } - /** * Writes files in 'this' directory to a tar stream. @@ -2284,6 +2560,10 @@ public final class FilePath implements Serializable { TarArchiveEntry te; while ((te = t.getNextTarEntry()) != null) { File f = new File(baseDir, te.getName()); + if (!f.toPath().normalize().startsWith(baseDir.toPath())) { + throw new IOException( + "Tar " + name + " contains illegal file name that breaks out of the target directory: " + te.getName()); + } if (te.isDirectory()) { mkdirs(f); } else { @@ -2360,7 +2640,7 @@ public final class FilePath implements Serializable { * Default bound for {@link #validateAntFileMask(String, int, boolean)}. * @since 1.592 */ - public static int VALIDATE_ANT_FILE_MASK_BOUND = SystemProperties.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); + public static int VALIDATE_ANT_FILE_MASK_BOUND = Integer.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); /** * Like {@link #validateAntFileMask(String)} but performing only a bounded number of operations. @@ -2375,9 +2655,20 @@ public final class FilePath implements Serializable { * @throws InterruptedException not only in case of a channel failure, but also if too many operations were performed without finding any matches * @since 1.484 */ - public String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException { - return act(new MasterToSlaveFileCallable() { + public @CheckForNull String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException { + return act(new ValidateAntFileMask(fileMasks, caseSensitive, bound)); + } + private class ValidateAntFileMask extends MasterToSlaveFileCallable { + private final String fileMasks; + private final boolean caseSensitive; + private final int bound; + ValidateAntFileMask(String fileMasks, boolean caseSensitive, int bound) { + this.fileMasks = fileMasks; + this.caseSensitive = caseSensitive; + this.bound = bound; + } private static final long serialVersionUID = 1; + @Override public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { if(fileMasks.startsWith("~")) return Messages.FilePath_TildaDoesntWork(); @@ -2523,7 +2814,6 @@ public final class FilePath implements Serializable { if(idx2==-1) return idx1; return Math.min(idx1,idx2); } - }); } private static final UrlFactory DEFAULT_URL_FACTORY = new UrlFactory(); @@ -2765,6 +3055,11 @@ public final class FilePath implements Serializable { return classLoader; } + @Override + public String toString() { + return callable.toString(); + } + private static final long serialVersionUID = 1L; } @@ -2789,11 +3084,13 @@ public final class FilePath implements Serializable { * (User's home directory in the Unix sense) of the given channel. */ public static FilePath getHomeDirectory(VirtualChannel ch) throws InterruptedException, IOException { - return ch.call(new MasterToSlaveCallable() { - public FilePath call() throws IOException { - return new FilePath(new File(System.getProperty("user.home"))); - } - }); + return ch.call(new GetHomeDirectory()); + } + private static class GetHomeDirectory extends MasterToSlaveCallable { + @Override + public FilePath call() throws IOException { + return new FilePath(new File(System.getProperty("user.home"))); + } } /** @@ -2923,11 +3220,12 @@ public final class FilePath implements Serializable { return f; } - private boolean mkdirs(File dir) { + private boolean mkdirs(File dir) throws IOException { if (dir.exists()) return false; filterNonNull().mkdirs(dir); - return dir.mkdirs(); + Files.createDirectories(fileToPath(dir)); + return true; } private File mkdirsE(File dir) throws IOException { @@ -2938,5 +3236,91 @@ public final class FilePath implements Serializable { return IOUtils.mkdirs(dir); } + /** + * Check if the relative child is really a descendant after symlink resolution if any. + * + * TODO un-restrict it in a weekly after the patch + */ + @Restricted(NoExternalUse.class) + public boolean isDescendant(@Nonnull String potentialChildRelativePath) throws IOException, InterruptedException { + return act(new IsDescendant(potentialChildRelativePath)); + } + + private class IsDescendant extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private String potentialChildRelativePath; + + private IsDescendant(@Nonnull String potentialChildRelativePath){ + this.potentialChildRelativePath = potentialChildRelativePath; + } + + @Override + public Boolean invoke(@Nonnull File parentFile, @Nonnull VirtualChannel channel) throws IOException, InterruptedException { + if (new File(potentialChildRelativePath).isAbsolute()) { + throw new IllegalArgumentException("Only a relative path is supported, the given path is absolute: " + potentialChildRelativePath); + } + + Path parentAbsolutePath = Util.fileToPath(parentFile.getAbsoluteFile()); + Path parentRealPath; + try { + parentRealPath = parentAbsolutePath.toRealPath(); + } + catch(NoSuchFileException e) { + throw new IllegalArgumentException("The parent does not exist"); + } + + // example: "a/b/c" that will become "b/c" then just "c", and finally an empty string + String remainingPath = potentialChildRelativePath; + + Path currentFilePath = parentFile.toPath(); + while (!remainingPath.isEmpty()) { + Path directChild = this.getDirectChild(currentFilePath, remainingPath); + Path childUsingFullPath = currentFilePath.resolve(remainingPath); + String childUsingFullPathAbs = childUsingFullPath.toAbsolutePath().toString(); + String directChildAbs = directChild.toAbsolutePath().toString(); + + if (childUsingFullPathAbs.length() == directChildAbs.length()) { + remainingPath = ""; + } else { + // +1 to avoid the last slash + remainingPath = childUsingFullPathAbs.substring(directChildAbs.length() + 1); + } + + File childFileSymbolic = Util.resolveSymlinkToFile(directChild.toFile()); + if (childFileSymbolic == null) { + currentFilePath = directChild; + } else { + currentFilePath = childFileSymbolic.toPath(); + } + + Path currentFileAbsolutePath = currentFilePath.toAbsolutePath(); + try{ + Path child = currentFileAbsolutePath.toRealPath(); + if (!child.startsWith(parentRealPath)) { + return false; + } + } catch (NoSuchFileException e) { + // nonexistent file + // in case this folder / file will be copied somewhere else, + // it becomes the responsibility of that system to check the isDescendant with the existing links + // we are not taking the parentRealPath to avoid possible problem + Path child = currentFileAbsolutePath.normalize(); + Path parent = parentAbsolutePath.normalize(); + return child.startsWith(parent); + } + } + + return true; + } + + private @CheckForNull Path getDirectChild(Path parentPath, String childPath){ + Path current = parentPath.resolve(childPath); + while (current != null && !parentPath.equals(current.getParent())) { + current = current.getParent(); + } + return current; + } + } + private static final SoloFilePathFilter UNRESTRICTED = SoloFilePathFilter.wrap(FilePathFilter.UNRESTRICTED); } diff --git a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java index 1e02e11f7f4dfdbedf2f00b853535725c1d43f86..9e162d666feb466d4487911fb4b2b2cdbd81a99a 100644 --- a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java +++ b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java @@ -50,12 +50,12 @@ public abstract class FileSystemProvisionerDescriptor extends Descriptortrue. + * perform the necessary deletion operation, and return {@code true}. * *

* If the workspace isn't the one created by this {@link FileSystemProvisioner}, or if the * workspace can be simply deleted by {@link FilePath#deleteRecursive()}, then simply - * return false to give other {@link FileSystemProvisionerDescriptor}s a chance to + * return {@code false} to give other {@link FileSystemProvisionerDescriptor}s a chance to * discard them. * * @param ws diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index e696d40f29c511fdbd4cb0279659c7c5fd287416..a2275d7d1fbbe8ad66808c8a92bd98d259ec5f16 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -26,6 +26,7 @@ package hudson; import hudson.model.Slave; +import hudson.security.*; import jenkins.util.SystemProperties; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; @@ -46,6 +47,7 @@ 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; @@ -56,11 +58,6 @@ import hudson.model.View; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.search.SearchableModelObject; -import hudson.security.AccessControlled; -import hudson.security.AuthorizationStrategy; -import hudson.security.GlobalSecurityConfiguration; -import hudson.security.Permission; -import hudson.security.SecurityRealm; import hudson.security.captcha.CaptchaSupport; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.Cloud; @@ -125,6 +122,7 @@ 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; @@ -136,7 +134,6 @@ import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu; -import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; import org.apache.commons.jelly.Script; @@ -399,11 +396,11 @@ public class Functions { * is chosen, this part remains intact. * *

- * The 524 is the path from {@link Job} to {@link Run}. + * The {@code 524} is the path from {@link Job} to {@link Run}. * *

- * The bbb portion is the path after that till the last - * {@link Run} subtype. The ccc portion is the part + * 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 { @@ -471,6 +468,16 @@ public class Functions { 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); } @@ -611,7 +618,7 @@ public class Functions { private static final SimpleFormatter formatter = new SimpleFormatter(); /** - * Used by layout.jelly to control the auto refresh behavior. + * 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 @@ -765,7 +772,7 @@ public class Functions { } /** - * This version is so that the 'checkPermission' on layout.jelly + * 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. */ @@ -1135,20 +1142,24 @@ public class Functions { * @since 1.512 */ public static List getAllTopLevelItems(ItemGroup root) { - return Items.getAllItems(root, TopLevelItem.class); + return root.getAllItems(TopLevelItem.class); } /** * Gets the relative name or display name to the given item from the specified group. * * @since 1.515 - * @param p the Item we want the relative display name - * @param g the ItemGroup used as point of reference for the item + * @param p the Item we want the relative display name. + * If {@code null}, a {@code null} will be returned by the method + * @param g the ItemGroup used as point of reference for the item. + * If the group is not specified, item's path will be used. * @param useDisplayName if true, returns a display name, otherwise returns a name * @return - * String like "foo » bar" + * String like "foo » bar". + * {@code null} if item is null or if one of its parents is not an {@link Item}. */ - public static String getRelativeNameFrom(Item p, ItemGroup g, boolean useDisplayName) { + @Nullable + public static String getRelativeNameFrom(@CheckForNull Item p, @CheckForNull ItemGroup g, boolean useDisplayName) { if (p == null) return null; if (g == null) return useDisplayName ? p.getFullDisplayName() : p.getFullName(); String separationString = useDisplayName ? " » " : "/"; @@ -1182,7 +1193,7 @@ public class Functions { if (gr instanceof Item) i = (Item)gr; - else + else // Parent is a group, but not an item return null; } } @@ -1192,11 +1203,14 @@ public class Functions { * * @since 1.515 * @param p the Item we want the relative display name + * If {@code null}, the method will immediately return {@code null}. * @param g the ItemGroup used as point of reference for the item * @return - * String like "foo/bar" + * String like "foo/bar". + * {@code null} if the item is {@code null} or if one of its parents is not an {@link Item}. */ - public static String getRelativeNameFrom(Item p, ItemGroup g) { + @Nullable + public static String getRelativeNameFrom(@CheckForNull Item p, @CheckForNull ItemGroup g) { return getRelativeNameFrom(p, g, false); } @@ -1205,12 +1219,15 @@ public class Functions { * Gets the relative display name to the given item from the specified group. * * @since 1.512 - * @param p the Item we want the relative display name + * @param p the Item we want the relative display name. + * If {@code null}, the method will immediately return {@code null}. * @param g the ItemGroup used as point of reference for the item * @return - * String like "Foo » Bar" + * String like "Foo » Bar". + * {@code null} if the item is {@code null} or if one of its parents is not an {@link Item}. */ - public static String getRelativeDisplayNameFrom(Item p, ItemGroup g) { + @Nullable + public static String getRelativeDisplayNameFrom(@CheckForNull Item p, @CheckForNull ItemGroup g) { return getRelativeNameFrom(p, g, true); } @@ -1361,6 +1378,7 @@ public class Functions { } public static String jsStringEscape(String s) { + if (s == null) return null; StringBuilder buf = new StringBuilder(); for( int i=0; i - * This is primarily used in slave-agent.jnlp.jelly to specify the destination + * This is primarily used in {@code slave-agent.jnlp.jelly} to specify the destination * that the agents talk to. */ public String getServerName() { @@ -1728,7 +1746,7 @@ public class Functions { /** * If the given href link is matching the current page, return true. * - * Used in task.jelly to decide if the page should be highlighted. + * Used in {@code task.jelly} to decide if the page should be highlighted. */ public boolean hyperlinkMatchesCurrentPage(String href) throws UnsupportedEncodingException { String url = Stapler.getCurrentRequest().getRequestURL().toString(); @@ -1753,7 +1771,18 @@ public class Functions { if(Jenkins.getInstanceOrNull()==null) return Collections.emptyList(); return PageDecorator.all(); } - + /** + * Gets only one {@link SimplePageDecorator}. + * @since 2.128 + */ + public static SimplePageDecorator getSimplePageDecorator() { + return SimplePageDecorator.first(); + } + + public static List getSimplePageDecorators() { + return SimplePageDecorator.all(); + } + public static List> getCloudDescriptors() { return Cloud.all(); } @@ -1802,7 +1831,7 @@ public class Functions { * from {@link ConsoleAnnotatorFactory}s and {@link ConsoleAnnotationDescriptor}s. */ public static String generateConsoleAnnotationScriptAndStylesheet() { - String cp = Stapler.getCurrentRequest().getContextPath(); + String cp = Stapler.getCurrentRequest().getContextPath() + Jenkins.RESOURCE_PATH; StringBuilder buf = new StringBuilder(); for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) { String path = cp + "/extensionList/" + ConsoleAnnotatorFactory.class.getName() + "/" + f.getClass().getName(); @@ -2024,7 +2053,7 @@ public class Functions { rsp.setHeader("X-Jenkins-Session", Jenkins.SESSION_HASH); TcpSlaveAgentListener tal = j.tcpSlaveAgentListener; - if (tal !=null) { + if (tal != null) { // headers used only by deprecated Remoting-based CLI int p = tal.getAdvertisedPort(); rsp.setIntHeader("X-Hudson-CLI-Port", p); rsp.setIntHeader("X-Jenkins-CLI-Port", p); @@ -2043,4 +2072,13 @@ public class Functions { } } + @Restricted(NoExternalUse.class) // for cc.xml.jelly + public static Collection getCCItems(View v) { + if (Stapler.getCurrentRequest().getParameter("recursive") != null) { + return v.getOwner().getItemGroup().getAllItems(TopLevelItem.class); + } else { + return v.getItems(); + } + } + } diff --git a/core/src/main/java/hudson/Indenter.java b/core/src/main/java/hudson/Indenter.java index c1f6971b6540a6b66c36f34e0378b605835bd290..aa4348b57a9bbc54b2a5b89fe272e4ed663562c5 100644 --- a/core/src/main/java/hudson/Indenter.java +++ b/core/src/main/java/hudson/Indenter.java @@ -26,7 +26,7 @@ package hudson; import hudson.model.Job; /** - * Used by projectView.jelly to indent modules. + * Used by {@code projectView.jelly} to indent modules. * * @author Kohsuke Kawaguchi */ diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index 696ddfd6229ae901f6e620d5511cb8932a29ff39..f0f18b4f81df12bb56440e68872bf0871fca1a55 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -26,6 +26,7 @@ package hudson; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Proc.LocalProc; import hudson.model.Computer; +import jenkins.util.MemoryReductionUtil; import hudson.util.QuotedStringTokenizer; import jenkins.model.Jenkins; import hudson.model.TaskListener; @@ -44,7 +45,6 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import javax.annotation.CheckForNull; -import javax.annotation.concurrent.GuardedBy; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; @@ -167,6 +167,8 @@ public abstract class Launcher { @CheckForNull protected OutputStream stdout = NULL_OUTPUT_STREAM, stderr; @CheckForNull + private TaskListener stdoutListener; + @CheckForNull protected InputStream stdin = NULL_INPUT_STREAM; @CheckForNull protected String[] envs = null; @@ -281,22 +283,25 @@ public abstract class Launcher { * Sets STDOUT destination. * * @param out Output stream. - * Use {@code null} to send STDOUT to /dev/null. + * Use {@code null} to send STDOUT to {@code /dev/null}. * @return {@code this} */ public ProcStarter stdout(@CheckForNull OutputStream out) { this.stdout = out; + stdoutListener = null; return this; } /** * Sends the stdout to the given {@link TaskListener}. * - * @param out Task listener + * @param out Task listener (must be safely remotable) * @return {@code this} */ public ProcStarter stdout(@Nonnull TaskListener out) { - return stdout(out.getLogger()); + stdout = out.getLogger(); + stdoutListener = out; + return this; } /** @@ -330,7 +335,7 @@ public abstract class Launcher { /** * Controls where the stdin of the process comes from. - * By default, /dev/null. + * By default, {@code /dev/null}. * * @return {@code this} */ @@ -392,7 +397,7 @@ public abstract class Launcher { */ @Nonnull public String[] envs() { - return envs != null ? envs.clone() : new String[0]; + return envs != null ? envs.clone() : MemoryReductionUtil.EMPTY_STRING_ARRAY; } /** @@ -491,6 +496,7 @@ public abstract class Launcher { @Nonnull public ProcStarter copy() { ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet); + rhs.stdoutListener = stdoutListener; rhs.reverseStdin = this.reverseStdin; rhs.reverseStderr = this.reverseStderr; rhs.reverseStdout = this.reverseStdout; @@ -1042,7 +1048,7 @@ public abstract class Launcher { } public Proc launch(ProcStarter ps) throws IOException { - final OutputStream out = ps.stdout == null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout)); + final OutputStream out = ps.stdout == null || ps.stdoutListener != null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout)); final OutputStream err = ps.stderr==null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stderr)); final InputStream in = (ps.stdin==null || ps.stdin==NULL_INPUT_STREAM) ? null : new RemoteInputStream(ps.stdin,false); @@ -1050,7 +1056,7 @@ public abstract class Launcher { final String workDir = psPwd==null ? null : psPwd.getRemote(); try { - return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener))); + return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener))); } catch (InterruptedException e) { throw (IOException)new InterruptedIOException().initCause(e); } @@ -1266,6 +1272,7 @@ public abstract class Launcher { private final @CheckForNull OutputStream err; private final @CheckForNull String workDir; private final @Nonnull TaskListener listener; + private final @CheckForNull TaskListener stdoutListener; private final boolean reverseStdin, reverseStdout, reverseStderr; private final boolean quiet; @@ -1273,7 +1280,7 @@ public abstract class Launcher { @CheckForNull InputStream in, boolean reverseStdin, @CheckForNull OutputStream out, boolean reverseStdout, @CheckForNull OutputStream err, boolean reverseStderr, - boolean quiet, @CheckForNull String workDir, @Nonnull TaskListener listener) { + boolean quiet, @CheckForNull String workDir, @Nonnull TaskListener listener, @CheckForNull TaskListener stdoutListener) { this.cmd = new ArrayList<>(cmd); this.masks = masks; this.env = env; @@ -1282,6 +1289,7 @@ public abstract class Launcher { this.err = err; this.workDir = workDir; this.listener = listener; + this.stdoutListener = stdoutListener; this.reverseStdin = reverseStdin; this.reverseStdout = reverseStdout; this.reverseStderr = reverseStderr; @@ -1289,8 +1297,14 @@ public abstract class Launcher { } public RemoteProcess call() throws IOException { + final Channel channel = getOpenChannelOrFail(); Launcher.ProcStarter ps = new LocalLauncher(listener).launch(); - ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err).quiet(quiet); + ps.cmds(cmd).masks(masks).envs(env).stdin(in).stderr(err).quiet(quiet); + if (stdoutListener != null) { + ps.stdout(stdoutListener.getLogger()); + } else { + ps.stdout(out); + } if(workDir!=null) ps.pwd(workDir); if (reverseStdin) ps.writeStdin(); if (reverseStdout) ps.readStdout(); @@ -1298,16 +1312,24 @@ public abstract class Launcher { final Proc p = ps.start(); - return Channel.current().export(RemoteProcess.class,new RemoteProcess() { + return channel.export(RemoteProcess.class,new RemoteProcess() { public int join() throws InterruptedException, IOException { try { return p.join(); } finally { // make sure I/O is delivered to the remote before we return + Channel taskChannel = null; try { - Channel.current().syncIO(); + // Sync IO will fail automatically if the channel is being closed, no need to use getOpenChannelOrFail() + // TODOL Replace by Channel#currentOrFail() when Remoting version allows + taskChannel = Channel.current(); + if (taskChannel == null) { + throw new IOException("No Remoting channel associated with this thread"); + } + taskChannel.syncIO(); } catch (Throwable t) { - // this includes a failure to sync, slave.jar too old, etc + // this includes a failure to sync, agent.jar too old, etc + LOGGER.log(Level.INFO, "Failed to synchronize IO streams on the channel " + taskChannel, t); } } } diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java index 896ae4dd53398525db56af00f19aeec7c40dde05..11cb516cee31be90a3f118b308f27179083f9d5a 100644 --- a/core/src/main/java/hudson/Main.java +++ b/core/src/main/java/hudson/Main.java @@ -144,7 +144,7 @@ public class Main { int ret; try (OutputStream os = Files.newOutputStream(tmpFile.toPath()); Writer w = new OutputStreamWriter(os,"UTF-8")) { - w.write(""); + w.write(""); w.write(""); w.flush(); diff --git a/core/src/main/java/hudson/Plugin.java b/core/src/main/java/hudson/Plugin.java index 0564c07d94a0f1a682f238b1fd950b14321247bc..2f065102becc4c8a3a247a9869a6ebb8a0150844 100644 --- a/core/src/main/java/hudson/Plugin.java +++ b/core/src/main/java/hudson/Plugin.java @@ -23,7 +23,7 @@ */ package hudson; -import hudson.util.TimeUnit2; +import java.util.concurrent.TimeUnit; import jenkins.model.Jenkins; import hudson.model.Descriptor; import hudson.model.Saveable; @@ -35,6 +35,7 @@ import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.File; @@ -42,10 +43,10 @@ import net.sf.json.JSONObject; import com.thoughtworks.xstream.XStream; import hudson.init.Initializer; import hudson.init.Terminator; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.URL; +import java.util.Locale; +import java.util.logging.Logger; import jenkins.model.GlobalConfiguration; -import org.kohsuke.stapler.HttpResponses; /** * Base class of Hudson plugin. @@ -61,26 +62,28 @@ import org.kohsuke.stapler.HttpResponses; * to plugin functionality. * *

- * A plugin is bound to URL space of Hudson as ${rootURL}/plugin/foo/, + * A plugin is bound to URL space of Hudson as {@code ${rootURL}/plugin/foo/}, * where "foo" is taken from your plugin name "foo.jpi". All your web resources * in src/main/webapp are visible from this URL, and you can also define Jelly * views against your Plugin class, and those are visible in this URL, too. * *

- * {@link Plugin} can have an optional config.jelly page. If present, + * {@link Plugin} can have an optional {@code config.jelly} page. If present, * it will become a part of the system configuration page (http://server/hudson/configure). * This is convenient for exposing/maintaining configuration that doesn't * fit any {@link Descriptor}s. * *

* Up until Hudson 1.150 or something, subclasses of {@link Plugin} required - * @plugin javadoc annotation, but that is no longer a requirement. + * {@code @plugin} javadoc annotation, but that is no longer a requirement. * * @author Kohsuke Kawaguchi * @since 1.42 */ public abstract class Plugin implements Saveable { + private static final Logger LOGGER = Logger.getLogger(Plugin.class.getName()); + /** * You do not need to create custom subtypes: *

    @@ -191,11 +194,11 @@ public abstract class Plugin implements Saveable { * Handles the submission for the system configuration. * *

    - * If this class defines config.jelly view, be sure to + * If this class defines {@code config.jelly} view, be sure to * override this method and persists the submitted values accordingly. * *

    - * The following is a sample config.jelly that you can start yours with: + * The following is a sample {@code config.jelly} that you can start yours with: *

    {@code 
          * <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
          *   <f:section title="Locale">
    @@ -219,32 +222,32 @@ public abstract class Plugin implements Saveable {
         }
     
         /**
    -     * This method serves static resources in the plugin under <tt>hudson/plugin/SHORTNAME</tt>.
    +     * This method serves static resources in the plugin under {@code hudson/plugin/SHORTNAME}.
          */
         public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
             String path = req.getRestOfPath();
     
    -        if (path.startsWith("/META-INF/") || path.startsWith("/WEB-INF/")) {
    -            throw HttpResponses.notFound();
    +        String pathUC = path.toUpperCase(Locale.ENGLISH);
    +        if (path.isEmpty() || path.contains("..") || path.startsWith(".") || path.contains("%")
    +                || pathUC.contains("META-INF") || pathUC.contains("WEB-INF")
    +                // ClassicPluginStrategy#explode produce that file to know if a new explosion is required or not
    +                || pathUC.equals("/.TIMESTAMP2")
    +        ) {
    +            LOGGER.warning("rejecting possibly malicious " + req.getRequestURIWithQueryString());
    +            rsp.sendError(HttpServletResponse.SC_BAD_REQUEST);
    +            return;
             }
     
    -        if(path.length()==0)
    -            path = "/";
    -
             // 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;
    +        long expires = staticLink ? TimeUnit.DAYS.toMillis(365) : -1;
     
             // use serveLocalizedFile to support automatic locale selection
    -        try {
    -            rsp.serveLocalizedFile(req, wrapper.baseResourceURL.toURI().resolve(new URI(null, '.' + path, null)).toURL(), expires);
    -        } catch (URISyntaxException x) {
    -            throw new IOException(x);
    -        }
    +        rsp.serveLocalizedFile(req, new URL(wrapper.baseResourceURL, '.' + path), expires);
         }
     
     //
    diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java
    index c7fd4e1b831d04513f24d390696a1ca4545ff9cc..d6f8f794f6e0142fb99510eab13716787e79a240 100644
    --- a/core/src/main/java/hudson/PluginManager.java
    +++ b/core/src/main/java/hudson/PluginManager.java
    @@ -25,8 +25,6 @@ package hudson;
     
     import edu.umd.cs.findbugs.annotations.NonNull;
     import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    -import hudson.security.ACLContext;
    -import jenkins.util.SystemProperties;
     import hudson.PluginWrapper.Dependency;
     import hudson.init.InitMilestone;
     import hudson.init.InitStrategy;
    @@ -36,22 +34,27 @@ import hudson.model.AbstractModelObject;
     import hudson.model.AdministrativeMonitor;
     import hudson.model.Api;
     import hudson.model.Descriptor;
    +import hudson.model.DownloadService;
     import hudson.model.Failure;
     import hudson.model.ItemGroupMixIn;
     import hudson.model.UpdateCenter;
    -import hudson.model.UpdateSite;
     import hudson.model.UpdateCenter.DownloadJob;
     import hudson.model.UpdateCenter.InstallationJob;
    +import hudson.model.UpdateSite;
     import hudson.security.ACL;
    +import hudson.security.ACLContext;
     import hudson.security.Permission;
     import hudson.security.PermissionScope;
     import hudson.util.CyclicGraphDetector;
     import hudson.util.CyclicGraphDetector.CycleDetectedException;
    +import hudson.util.FormValidation;
     import hudson.util.PersistedList;
    +import hudson.util.Retrier;
     import hudson.util.Service;
     import hudson.util.VersionNumber;
     import hudson.util.XStream2;
     import jenkins.ClassLoaderReflectionToolkit;
    +import jenkins.ExtensionRefreshException;
     import jenkins.InitReactorRunner;
     import jenkins.MissingDependencyException;
     import jenkins.RestartRequiredException;
    @@ -59,12 +62,12 @@ import jenkins.YesNoMaybe;
     import jenkins.install.InstallState;
     import jenkins.install.InstallUtil;
     import jenkins.model.Jenkins;
    +import jenkins.security.CustomClassFilter;
    +import jenkins.util.SystemProperties;
     import jenkins.util.io.OnMaster;
     import jenkins.util.xml.RestrictiveEntityResolver;
    -
     import net.sf.json.JSONArray;
     import net.sf.json.JSONObject;
    -
     import org.acegisecurity.Authentication;
     import org.apache.commons.fileupload.FileItem;
     import org.apache.commons.fileupload.FileUploadException;
    @@ -82,17 +85,24 @@ import org.jvnet.hudson.reactor.Reactor;
     import org.jvnet.hudson.reactor.ReactorException;
     import org.jvnet.hudson.reactor.TaskBuilder;
     import org.jvnet.hudson.reactor.TaskGraphBuilder;
    +import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.DoNotUse;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.HttpRedirect;
     import org.kohsuke.stapler.HttpResponse;
     import org.kohsuke.stapler.HttpResponses;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerOverridable;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.export.Exported;
     import org.kohsuke.stapler.export.ExportedBean;
     import org.kohsuke.stapler.interceptor.RequirePOST;
    +import org.xml.sax.Attributes;
    +import org.xml.sax.InputSource;
    +import org.xml.sax.SAXException;
    +import org.xml.sax.helpers.DefaultHandler;
     
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
    @@ -100,6 +110,7 @@ import javax.servlet.ServletContext;
     import javax.servlet.ServletException;
     import javax.xml.parsers.ParserConfigurationException;
     import javax.xml.parsers.SAXParserFactory;
    +import java.io.ByteArrayInputStream;
     import java.io.Closeable;
     import java.io.File;
     import java.io.FilenameFilter;
    @@ -107,10 +118,12 @@ import java.io.IOException;
     import java.io.InputStream;
     import java.lang.ref.WeakReference;
     import java.lang.reflect.Method;
    +import java.net.JarURLConnection;
     import java.net.MalformedURLException;
     import java.net.URISyntaxException;
     import java.net.URL;
     import java.net.URLClassLoader;
    +import java.net.URLConnection;
     import java.util.ArrayList;
     import java.util.Collection;
     import java.util.Collections;
    @@ -120,8 +133,8 @@ import java.util.HashSet;
     import java.util.Iterator;
     import java.util.LinkedHashSet;
     import java.util.List;
    -import java.util.ListIterator;
     import java.util.Map;
    +import java.util.ServiceLoader;
     import java.util.Set;
     import java.util.TreeMap;
     import java.util.UUID;
    @@ -129,29 +142,15 @@ import java.util.concurrent.ConcurrentHashMap;
     import java.util.concurrent.ConcurrentMap;
     import java.util.concurrent.CopyOnWriteArrayList;
     import java.util.concurrent.Future;
    +import java.util.function.Supplier;
    +import java.util.jar.JarEntry;
     import java.util.jar.JarFile;
     import java.util.jar.Manifest;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    -import org.xml.sax.Attributes;
    -import org.xml.sax.InputSource;
    -import org.xml.sax.SAXException;
    -import org.xml.sax.helpers.DefaultHandler;
     
     import static hudson.init.InitMilestone.*;
    -import hudson.model.DownloadService;
    -import hudson.util.FormValidation;
    -import java.io.ByteArrayInputStream;
    -import java.net.JarURLConnection;
    -import java.net.URLConnection;
    -import java.util.jar.JarEntry;
    -
    -import static java.util.logging.Level.FINE;
    -import static java.util.logging.Level.INFO;
    -import static java.util.logging.Level.SEVERE;
    -import static java.util.logging.Level.WARNING;
    -import org.kohsuke.accmod.Restricted;
    -import org.kohsuke.accmod.restrictions.NoExternalUse;
    +import static java.util.logging.Level.*;
     
     /**
      * Manages {@link PluginWrapper}s.
    @@ -175,10 +174,35 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
      * @author Kohsuke Kawaguchi
      */
     @ExportedBean
    -public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable {
    +public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable, StaplerProxy {
         /** Custom plugin manager system property or context param. */
         public static final String CUSTOM_PLUGIN_MANAGER = PluginManager.class.getName() + ".className";
     
    +    private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());
    +
    +    /**
    +     * Time elapsed between retries to check the updates sites. It's kind of constant, but let it so for tests
    +     */
    +    /* private final */ static int CHECK_UPDATE_SLEEP_TIME_MILLIS;
    +
    +    /**
    +     * Number of attempts to check the updates sites. It's kind of constant, but let it so for tests
    +     */
    +    /* private final */ static int CHECK_UPDATE_ATTEMPTS;
    +
    +    static {
    +        try {
    +            // Secure initialization
    +            CHECK_UPDATE_SLEEP_TIME_MILLIS = SystemProperties.getInteger(PluginManager.class.getName() + ".checkUpdateSleepTimeMillis", 1000);
    +            CHECK_UPDATE_ATTEMPTS = SystemProperties.getInteger(PluginManager.class.getName() + ".checkUpdateAttempts", 1);
    +        } catch(Exception e) {
    +            LOGGER.warning(String.format("There was an error initializing the PluginManager. Exception: %s", e));
    +        } finally {
    +            CHECK_UPDATE_ATTEMPTS = CHECK_UPDATE_ATTEMPTS > 0 ? CHECK_UPDATE_ATTEMPTS : 1;
    +            CHECK_UPDATE_SLEEP_TIME_MILLIS = CHECK_UPDATE_SLEEP_TIME_MILLIS > 0 ? CHECK_UPDATE_SLEEP_TIME_MILLIS : 1000;
    +        }
    +    }
    +
         /** Accepted constructors for custom plugin manager, in the order they are tried. */
         private enum PMConstructor {
             JENKINS {
    @@ -254,7 +278,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
         /**
          * All discovered plugins.
          */
    -    protected final List<PluginWrapper> plugins = new ArrayList<PluginWrapper>();
    +    protected final List<PluginWrapper> plugins = new CopyOnWriteArrayList<>();
     
         /**
          * All active plugins, topologically sorted so that when X depends on Y, Y appears in the list before X does.
    @@ -268,6 +292,12 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
          */
         public final File rootDir;
     
    +    /**
    +     * Hold the status of the last try to check update centers. Consumed from the check.jelly to show an
    +     * error message if the last attempt failed.
    +     */
    +    private String lastErrorCheckUpdateCenters = null;
    +
         /**
          * If non-null, the base directory for all exploded .hpi/.jpi plugins. Controlled by the system property / servlet
          * context parameter {@literal hudson.PluginManager.workDir}.
    @@ -463,10 +493,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                                             cgd.run(getPlugins());
     
                                             // obtain topologically sorted list and overwrite the list
    -                                        ListIterator<PluginWrapper> litr = getPlugins().listIterator();
                                             for (PluginWrapper p : cgd.getSorted()) {
    -                                            litr.next();
    -                                            litr.set(p);
                                                 if(p.isActive())
                                                     activePlugins.add(p);
                                             }
    @@ -653,7 +680,17 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                         continue;
                     }
     
    -                String artifactId = dependencyToken.split(":")[0];
    +                String[] artifactIdVersionPair = dependencyToken.split(":");
    +                String artifactId = artifactIdVersionPair[0];
    +                VersionNumber dependencyVersion = new VersionNumber(artifactIdVersionPair[1]);
    +
    +                PluginManager manager = Jenkins.getActiveInstance().getPluginManager();
    +                VersionNumber installedVersion = manager.getPluginVersion(manager.rootDir, artifactId);
    +                if (installedVersion != null && !installedVersion.isOlderThan(dependencyVersion)) {
    +                    // Do not downgrade dependencies that are already installed.
    +                    continue;
    +                }
    +
                     URL dependencyURL = context.getResource(fromPath + "/" + artifactId + ".hpi");
     
                     if (dependencyURL == null) {
    @@ -682,9 +719,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
          * </ul>
          */
         protected void loadDetachedPlugins() {
    -        InstallState installState = Jenkins.getActiveInstance().getInstallState();
    -        if (InstallState.UPGRADE.equals(installState)) {
    -            VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion());
    +        VersionNumber lastExecVersion = new VersionNumber(InstallUtil.getLastExecVersion());
    +        if (lastExecVersion.isNewerThan(InstallUtil.NEW_INSTALL_VERSION) && lastExecVersion.isOlderThan(Jenkins.getVersion())) {
     
                 LOGGER.log(INFO, "Upgrading Jenkins. The last running version was {0}. This Jenkins is version {1}.",
                         new Object[] {lastExecVersion, Jenkins.VERSION});
    @@ -699,13 +735,14 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                         // If this was a plugin that was detached some time in the past i.e. not just one of the
                         // plugins that was bundled "for fun".
                         if (ClassicPluginStrategy.isDetachedPlugin(name)) {
    -                        // If it's already installed and the installed version is older
    -                        // than the bundled version, then we upgrade. The bundled version is the min required version
    -                        // for "this" version of Jenkins, so we must upgrade.
                             VersionNumber installedVersion = getPluginVersion(rootDir, name);
                             VersionNumber bundledVersion = getPluginVersion(dir, name);
    -                        if (installedVersion != null && bundledVersion != null && installedVersion.isOlderThan(bundledVersion)) {
    -                            return true;
    +                        // If the plugin is already installed, we need to decide whether to replace it with the bundled version.
    +                        if (installedVersion != null && bundledVersion != null) {
    +                            // If the installed version is older than the bundled version, then it MUST be upgraded.
    +                            // If the installed version is newer than the bundled version, then it MUST NOT be upgraded.
    +                            // If the versions are equal we just keep the installed version.
    +                            return installedVersion.isOlderThan(bundledVersion);
                             }
                         }
     
    @@ -861,6 +898,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                     ((UberClassLoader) uberClassLoader).loaded.clear();
                 }
     
    +            // TODO antimodular; perhaps should have a PluginListener to complement ExtensionListListener?
    +            CustomClassFilter.Contributed.load();
    +
                 try {
                     p.resolvePluginDependencies();
                     strategy.load(p);
    @@ -909,6 +949,11 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                 // Redo who depends on who.
                 resolveDependantPlugins();
     
    +            try {
    +                Jenkins.get().refreshExtensions();
    +            } catch (ExtensionRefreshException e) {
    +                throw new IOException("Failed to refresh extensions after installing " + sn + " plugin", e);
    +            }
                 LOGGER.info("Plugin " + p.getShortName()+":"+p.getVersion() + " dynamically installed");
             }
         }
    @@ -916,8 +961,15 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
         @Restricted(NoExternalUse.class)
         public synchronized void resolveDependantPlugins() {
             for (PluginWrapper plugin : plugins) {
    +            // Set of optional dependants plugins of plugin
    +            Set<String> optionalDependants = new HashSet<>();
                 Set<String> dependants = new HashSet<>();
                 for (PluginWrapper possibleDependant : plugins) {
    +                // No need to check if plugin is dependant of itself
    +                if(possibleDependant.getShortName().equals(plugin.getShortName())) {
    +                    continue;
    +                }
    +
                     // The plugin could have just been deleted. If so, it doesn't
                     // count as a dependant.
                     if (possibleDependant.isDeleted()) {
    @@ -927,10 +979,20 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                     for (Dependency dependency : dependencies) {
                         if (dependency.shortName.equals(plugin.getShortName())) {
                             dependants.add(possibleDependant.getShortName());
    +
    +                        // If, in addition, the dependency is optional, add to the optionalDependants list
    +                        if (dependency.optional) {
    +                            optionalDependants.add(possibleDependant.getShortName());
    +                        }
    +
    +                        // already know possibleDependant depends on plugin, no need to continue with the rest of
    +                        // dependencies. We continue with the next possibleDependant
    +                        break;
                         }
                     }
                 }
                 plugin.setDependants(dependants);
    +            plugin.setOptionalDependants(optionalDependants);
             }
         }
     
    @@ -1132,9 +1194,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
          */
         @Exported
         public List<PluginWrapper> getPlugins() {
    -        List<PluginWrapper> out = new ArrayList<PluginWrapper>(plugins.size());
    -        out.addAll(plugins);
    -        return out;
    +        return Collections.unmodifiableList(plugins);
         }
     
         public List<FailedPlugin> getFailedPlugins() {
    @@ -1199,8 +1259,10 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
     
         /**
          * Discover all the service provider implementations of the given class,
    -     * via <tt>META-INF/services</tt>.
    +     * via {@code META-INF/services}.
    +     * @deprecated Use {@link ServiceLoader} instead, or (more commonly) {@link ExtensionList}.
          */
    +    @Deprecated
         public <T> Collection<Class<? extends T>> discover( Class<T> spi ) {
             Set<Class<? extends T>> result = new HashSet<Class<? extends T>>();
     
    @@ -1474,7 +1536,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                         }
                         updateCenter.persistInstallStatus();
                         if(!failures) {
    -                        try (ACLContext _ = ACL.as(currentAuth)) {
    +                        try (ACLContext acl = ACL.as(currentAuth)) {
                                 InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_PLUGINS_INSTALLING);
                             }
                         }
    @@ -1624,24 +1686,86 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
         @Restricted(NoExternalUse.class)
         @RequirePOST public HttpResponse doCheckUpdatesServer() throws IOException {
             Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +
    +        // We'll check the update servers with a try-retry mechanism. The retrier is built with a builder
    +        Retrier<FormValidation> updateServerRetrier = new Retrier.Builder<>(
    +                // the action to perform
    +                this::checkUpdatesServer,
    +
    +                // the way we know whether this attempt was right or wrong
    +                (currentAttempt, result) -> result.kind == FormValidation.Kind.OK,
    +
    +                // the action name we are trying to perform
    +                "check updates server")
    +
    +                // the number of attempts to try
    +                .withAttempts(CHECK_UPDATE_ATTEMPTS)
    +
    +                // the delay between attempts
    +                .withDelay(CHECK_UPDATE_SLEEP_TIME_MILLIS)
    +
    +                // whatever exception raised is considered as a fail attempt (all exceptions), not a failure
    +                .withDuringActionExceptions(new Class[] {Exception.class})
    +
    +                // what we do with a failed attempt due to an allowed exception, return an FormValidation.error with the message
    +                .withDuringActionExceptionListener( (attempt, e) -> FormValidation.errorWithMarkup(e.getClass().getSimpleName() + ": " + e.getLocalizedMessage()))
    +
    +                // lets get our retrier object
    +                .build();
    +
             try {
    -            for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) {
    -                FormValidation v = site.updateDirectlyNow(DownloadService.signatureCheck);
    -                if (v.kind != FormValidation.Kind.OK) {
    -                    // TODO crude but enough for now
    -                    return v;
    +            // Begin the process
    +            FormValidation result = updateServerRetrier.start();
    +
    +            // Check how it went
    +            if (!FormValidation.Kind.OK.equals(result.kind)) {
    +                LOGGER.log(Level.SEVERE, Messages.PluginManager_UpdateSiteError(CHECK_UPDATE_ATTEMPTS, result.getMessage()));
    +                if (CHECK_UPDATE_ATTEMPTS > 1 && !Logger.getLogger(Retrier.class.getName()).isLoggable(Level.WARNING)) {
    +                    LOGGER.log(Level.SEVERE, Messages.PluginManager_UpdateSiteChangeLogLevel(Retrier.class.getName()));
                     }
    +
    +                lastErrorCheckUpdateCenters = Messages.PluginManager_CheckUpdateServerError(result.getMessage());
    +            } else {
    +                lastErrorCheckUpdateCenters = null;
                 }
    -            for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) {
    -                FormValidation v = d.updateNow();
    -                if (v.kind != FormValidation.Kind.OK) {
    -                    return v;
    -                }
    +
    +        } catch (Exception e) {
    +            // It's never going to be reached because we declared all Exceptions in the withDuringActionExceptions, so
    +            // whatever exception is considered a expected failed attempt and the retries continue
    +            LOGGER.log(Level.WARNING, Messages.PluginManager_UnexpectedException(), e);
    +
    +            // In order to leave this method as it was, rethrow as IOException
    +            throw new IOException(e);
    +        }
    +
    +        // Stay in the same page in any case
    +        return HttpResponses.forwardToPreviousPage();
    +    }
    +
    +    private FormValidation checkUpdatesServer() throws Exception {
    +        for (UpdateSite site : Jenkins.get().getUpdateCenter().getSites()) {
    +            FormValidation v = site.updateDirectlyNow(DownloadService.signatureCheck);
    +            if (v.kind != FormValidation.Kind.OK) {
    +                // Stop with an error
    +                return v;
    +            }
    +        }
    +        for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) {
    +            FormValidation v = d.updateNow();
    +            if (v.kind != FormValidation.Kind.OK) {
    +                // Stop with an error
    +                return v;
                 }
    -            return HttpResponses.forwardToPreviousPage();
    -        } catch(RuntimeException ex) {
    -            throw new IOException("Unhandled exception during updates server check", ex);
             }
    +        return FormValidation.ok();
    +    }
    +
    +    /**
    +     * Returns the last error raised during the update sites checking.
    +     * @return the last error message
    +     */
    +    public String getLastErrorCheckUpdateCenters() {
    +        return lastErrorCheckUpdateCenters;
         }
     
         protected String identifyPluginShortName(File t) {
    @@ -1689,7 +1813,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
             for (Map.Entry<String,VersionNumber> requestedPlugin : parseRequestedPlugins(configXml).entrySet()) {
                 PluginWrapper pw = getPlugin(requestedPlugin.getKey());
                 if (pw == null) { // install new
    -                UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey());
    +                UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue());
                     if (toInstall == null) {
                         LOGGER.log(WARNING, "No such plugin {0} to install", requestedPlugin.getKey());
                         continue;
    @@ -1700,9 +1824,12 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                     if (toInstall.isForNewerHudson()) {
                         LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
                     }
    +                if (toInstall.isForNewerJava()) {
    +                    LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version});
    +                }
                     jobs.add(toInstall.deploy(true));
                 } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade
    -                UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey());
    +                UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue());
                     if (toInstall == null) {
                         LOGGER.log(WARNING, "No such plugin {0} to upgrade", requestedPlugin.getKey());
                         continue;
    @@ -1717,6 +1844,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                     if (toInstall.isForNewerHudson()) {
                         LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
                     }
    +                if (toInstall.isForNewerJava()) {
    +                    LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version});
    +                }
                     if (!toInstall.isCompatibleWithInstalledVersion()) {
                         LOGGER.log(WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()});
                     }
    @@ -1805,6 +1935,44 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
             return requestedPlugins;
         }
     
    +    @Restricted(DoNotUse.class) // table.jelly
    +    public MetadataCache createCache() {
    +        return new MetadataCache();
    +    }
    +
    +    /**
    +     * Disable a list of plugins using a strategy for their dependants plugins.
    +     * @param strategy the strategy regarding how the dependant plugins are processed
    +     * @param plugins the list of plugins
    +     * @return the list of results for every plugin and their dependant plugins.
    +     * @throws IOException see {@link PluginWrapper#disable()}
    +     */
    +    public @NonNull List<PluginWrapper.PluginDisableResult> disablePlugins(@NonNull PluginWrapper.PluginDisableStrategy strategy, @NonNull List<String> plugins) throws IOException {
    +        // Where we store the results of each plugin disablement
    +        List<PluginWrapper.PluginDisableResult> results = new ArrayList<>(plugins.size());
    +
    +        // Disable all plugins passed
    +        for (String pluginName : plugins) {
    +            PluginWrapper plugin = this.getPlugin(pluginName);
    +
    +            if (plugin == null) {
    +                results.add(new PluginWrapper.PluginDisableResult(pluginName, PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(pluginName)));
    +            } else {
    +                results.add(plugin.disable(strategy));
    +            }
    +        }
    +
    +        return results;
    +    }
    +
    +    @Restricted(NoExternalUse.class) // table.jelly
    +    public static final class MetadataCache {
    +        private final Map<String, Object> data = new HashMap<>();
    +        public <T> T of(String key, Class<T> type, Supplier<T> func) {
    +            return type.cast(data.computeIfAbsent(key, _ignored -> func.get()));
    +        }
    +    }
    +
         /**
          * {@link ClassLoader} that can see all plugins.
          */
    @@ -1922,9 +2090,6 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
                 return "classLoader " +  getClass().getName();
             }
         }
    -
    -    private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());
    -
         public static boolean FAST_LOOKUP = !SystemProperties.getBoolean(PluginManager.class.getName()+".noFastLookup");
     
         public static final Permission UPLOAD_PLUGINS = new Permission(Jenkins.PERMISSIONS, "UploadPlugins", Messages._PluginManager_UploadPluginsPermission_Description(),Jenkins.ADMINISTER,PermissionScope.JENKINS);
    @@ -2000,8 +2165,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
              * Convenience method to ease access to this monitor, this allows other plugins to register required updates.
              * @return this monitor.
              */
    -        public static final PluginUpdateMonitor getInstance() {
    -            return ExtensionList.lookup(PluginUpdateMonitor.class).get(0);
    +        public static PluginUpdateMonitor getInstance() {
    +            return ExtensionList.lookupSingleton(PluginUpdateMonitor.class);
             }
     
             /**
    @@ -2052,4 +2217,19 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
             }
     
         }
    +
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(PluginManager.class.getName() + ".skipPermissionCheck");
     }
    diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java
    index 9ca85b04aa4e694e57b3357c49a86172dbda8d03..b9cdd610db771c0ebd07c1b5f8c23fbe53ec328e 100644
    --- a/core/src/main/java/hudson/PluginWrapper.java
    +++ b/core/src/main/java/hudson/PluginWrapper.java
    @@ -25,18 +25,22 @@
     package hudson;
     
     import com.google.common.collect.ImmutableSet;
    +import com.google.common.collect.Sets;
     import hudson.PluginManager.PluginInstanceStore;
     import hudson.model.AdministrativeMonitor;
     import hudson.model.Api;
     import hudson.model.ModelObject;
    -import java.nio.file.Files;
    -import java.nio.file.InvalidPathException;
    -import jenkins.YesNoMaybe;
    -import jenkins.model.Jenkins;
     import hudson.model.UpdateCenter;
     import hudson.model.UpdateSite;
     import hudson.util.VersionNumber;
    -import org.jvnet.localizer.ResourceBundleHolder;
    +import jenkins.YesNoMaybe;
    +import jenkins.model.Jenkins;
    +import jenkins.util.java.JavaUtils;
    +import org.apache.commons.lang.StringUtils;
    +import org.apache.commons.logging.LogFactory;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.DoNotUse;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.HttpResponse;
     import org.kohsuke.stapler.HttpResponses;
     import org.kohsuke.stapler.StaplerRequest;
    @@ -45,16 +49,15 @@ import org.kohsuke.stapler.export.Exported;
     import org.kohsuke.stapler.export.ExportedBean;
     import org.kohsuke.stapler.interceptor.RequirePOST;
     
    -import org.apache.commons.lang.StringUtils;
    -import org.apache.commons.logging.LogFactory;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import java.io.Closeable;
     import java.io.File;
    -import java.io.FileOutputStream;
     import java.io.IOException;
     import java.io.OutputStream;
     import java.net.URL;
    +import java.nio.file.Files;
    +import java.nio.file.InvalidPathException;
     import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collection;
    @@ -65,12 +68,20 @@ import java.util.HashSet;
     import java.util.Iterator;
     import java.util.List;
     import java.util.Map;
    +import java.util.Objects;
     import java.util.Set;
    +import java.util.function.Predicate;
     import java.util.jar.JarFile;
     import java.util.jar.Manifest;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +import java.util.stream.Collectors;
     
    +import static hudson.PluginWrapper.PluginDisableStatus.ALREADY_DISABLED;
    +import static hudson.PluginWrapper.PluginDisableStatus.DISABLED;
    +import static hudson.PluginWrapper.PluginDisableStatus.ERROR_DISABLING;
    +import static hudson.PluginWrapper.PluginDisableStatus.NOT_DISABLED_DEPENDANTS;
    +import static hudson.PluginWrapper.PluginDisableStatus.NO_SUCH_PLUGIN;
     import static java.util.logging.Level.WARNING;
     import static org.apache.commons.io.FilenameUtils.getBaseName;
     
    @@ -79,7 +90,7 @@ import static org.apache.commons.io.FilenameUtils.getBaseName;
      * for Jenkins to control {@link Plugin}.
      *
      * <p>
    - * A plug-in is packaged into a jar file whose extension is <tt>".jpi"</tt> (or <tt>".hpi"</tt> for backward compatibility),
    + * A plug-in is packaged into a jar file whose extension is {@code ".jpi"} (or {@code ".hpi"} for backward compatibility),
      * A plugin needs to have a special manifest entry to identify what it is.
      *
      * <p>
    @@ -124,7 +135,7 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
         /**
          * Base URL for loading static resources from this plugin.
          * Null if disabled. The static resources are mapped under
    -     * <tt>CONTEXTPATH/plugin/SHORTNAME/</tt>.
    +     * {@code CONTEXTPATH/plugin/SHORTNAME/}.
          */
         public final URL baseResourceURL;
     
    @@ -149,7 +160,7 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
     
         /**
          * True if this plugin is activated for this session.
    -     * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
    +     * The snapshot of {@code disableFile.exists()} as of the start up.
          */
         private final boolean active;
         
    @@ -159,10 +170,34 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
         private final List<Dependency> optionalDependencies;
     
         public List<String> getDependencyErrors() {
    -        return Collections.unmodifiableList(dependencyErrors);
    +        return Collections.unmodifiableList(new ArrayList<>(dependencyErrors.keySet()));
    +    }
    +
    +    @Restricted(NoExternalUse.class) // Jelly use
    +    public List<String> getOriginalDependencyErrors() {
    +        Predicate<Map.Entry<String, Boolean>> p = Map.Entry::getValue;
    +        return dependencyErrors.entrySet().stream().filter(p.negate()).map(Map.Entry::getKey).collect(Collectors.toList());
    +    }
    +
    +    @Restricted(NoExternalUse.class) // Jelly use
    +    public boolean hasOriginalDependencyErrors() {
    +        return !getOriginalDependencyErrors().isEmpty();
         }
     
    -    private final transient List<String> dependencyErrors = new ArrayList<>();
    +    @Restricted(NoExternalUse.class) // Jelly use
    +    public List<String> getDerivedDependencyErrors() {
    +        return dependencyErrors.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toList());
    +    }
    +
    +    @Restricted(NoExternalUse.class) // Jelly use
    +    public boolean hasDerivedDependencyErrors() {
    +        return !getDerivedDependencyErrors().isEmpty();
    +    }
    +
    +    /**
    +     * A String error message, and a boolean indicating whether it's an original error (false) or downstream from an original one (true)
    +     */
    +    private final transient Map<String, Boolean> dependencyErrors = new HashMap<>(0);
     
         /**
          * Is this plugin bundled in jenkins.war?
    @@ -174,6 +209,11 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
          */
         private Set<String> dependants = Collections.emptySet();
     
    +    /**
    +     * List of plugins that depend optionally on this plugin.
    +     */
    +    private Set<String> optionalDependants = Collections.emptySet();
    +
         /**
          * The core can depend on a plugin if it is bundled. Sometimes it's the only thing that
          * depends on the plugin e.g. UI support library bundle plugin.
    @@ -188,6 +228,14 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
             this.dependants = dependants;
         }
     
    +    /**
    +     * Set the list of components that depend optionally on this plugin.
    +     * @param optionalDependants The list of components that depend optionally on this plugin.
    +     */
    +    public void setOptionalDependants(@Nonnull Set<String> optionalDependants) {
    +        this.optionalDependants = optionalDependants;
    +    }
    +
         /**
          * Get the list of components that depend on this plugin.
          * @return The list of components that depend on this plugin.
    @@ -200,6 +248,13 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
             }
         }
     
    +    /**
    +     * @return The list of components that depend optionally on this plugin.
    +     */
    +    public @Nonnull Set<String> getOptionalDependants() {
    +        return optionalDependants;
    +    }
    +
         /**
          * Does this plugin have anything that depends on it.
          * @return {@code true} if something (Jenkins core, or another plugin) depends on this
    @@ -208,7 +263,17 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
         public boolean hasDependants() {
             return (isBundled || !dependants.isEmpty());
         }
    -    
    +
    +    /**
    +     * Does this plugin have anything that depends optionally on it.
    +     * @return {@code true} if something (Jenkins core, or another plugin) depends optionally on this
    +     * plugin, otherwise {@code false}.
    +     */
    +    public boolean hasOptionalDependants() {
    +        return !optionalDependants.isEmpty();
    +    }
    +
    +
         /**
          * Does this plugin depend on any other plugins.
          * @return {@code true} if this plugin depends on other plugins, otherwise {@code false}.
    @@ -230,8 +295,8 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
                 int idx = s.indexOf(':');
                 if(idx==-1)
                     throw new IllegalArgumentException("Illegal dependency specifier "+s);
    -            this.shortName = s.substring(0,idx);
    -            String version = s.substring(idx+1);
    +            this.shortName = Util.intern(s.substring(0,idx));
    +            String version = Util.intern(s.substring(idx+1));
     
                 boolean isOptional = false;
                 String[] osgiProperties = version.split("[;]");
    @@ -274,7 +339,7 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
     			List<Dependency> dependencies, List<Dependency> optionalDependencies) {
             this.parent = parent;
     		this.manifest = manifest;
    -		this.shortName = computeShortName(manifest, archive.getName());
    +		this.shortName = Util.intern(computeShortName(manifest, archive.getName()));
     		this.baseResourceURL = baseResourceURL;
     		this.classLoader = classLoader;
     		this.disableFile = disableFile;
    @@ -431,6 +496,20 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
             return null;
         }
     
    +    /**
    +     * Returns the minimum Java version of this plugin, as specified in the plugin metadata.
    +     * Generally coming from the <code>java.level</code> extracted as MANIFEST's metadata with
    +     * <a href="https://github.com/jenkinsci/plugin-pom/pull/134">this addition on the plugins' parent pom</a>.
    +     *
    +     * @see <a href="https://github.com/jenkinsci/maven-hpi-plugin/pull/75">maven-hpi-plugin#PR-75</a>.
    +     *
    +     * @since TODO
    +     */
    +    @Exported
    +    public @CheckForNull String getMinimumJavaVersion() {
    +        return manifest.getMainAttributes().getValue("Minimum-Java-Version");
    +    }
    +
         /**
          * Returns the version number of this plugin
          */
    @@ -493,9 +572,19 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
         }
     
         /**
    -     * Disables this plugin next time Jenkins runs.
    +     * Disables this plugin next time Jenkins runs. As it doesn't check anything, it's recommended to use the method
    +     * {@link #disable(PluginDisableStrategy)}
          */
    +    @Deprecated //see https://issues.jenkins-ci.org/browse/JENKINS-27177
         public void disable() throws IOException {
    +        disableWithoutCheck();
    +    }
    +
    +    /**
    +     * Disable a plugin wihout checking any dependency. Only add the disable file.
    +     * @throws IOException
    +     */
    +    private void disableWithoutCheck() throws IOException {
             // creates an empty file
             try (OutputStream os = Files.newOutputStream(disableFile.toPath())) {
                 os.close();
    @@ -504,6 +593,103 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
             }
         }
     
    +    /**
    +     * Disable this plugin using a strategy.
    +     * @param strategy strategy to use
    +     * @return an object representing the result of the disablement of this plugin and its dependants plugins.
    +     */
    +    public @Nonnull PluginDisableResult disable(@Nonnull PluginDisableStrategy strategy) {
    +        PluginDisableResult result = new PluginDisableResult(shortName);
    +
    +        if (!this.isEnabled()) {
    +            result.setMessage(Messages.PluginWrapper_Already_Disabled(shortName));
    +            result.setStatus(ALREADY_DISABLED);
    +            return result;
    +        }
    +
    +        // Act as a flag indicating if this plugin, finally, can be disabled. If there is a not-disabled-dependant
    +        // plugin, this one couldn't be disabled.
    +        String aDependantNotDisabled = null;
    +
    +        // List of dependants plugins to 'check'. 'Check' means disable for mandatory or all strategies, or review if
    +        // this dependant-mandatory plugin is enabled in order to return an error for the NONE strategy.
    +        Set<String> dependantsToCheck = dependantsToCheck(strategy);
    +
    +        // Review all the dependants and add to the plugin result what happened with its dependants
    +        for (String dependant : dependantsToCheck) {
    +            PluginWrapper dependantPlugin = parent.getPlugin(dependant);
    +
    +            // The dependant plugin doesn't exist, add an error to the report
    +            if (dependantPlugin == null) {
    +                PluginDisableResult dependantStatus = new PluginDisableResult(dependant, NO_SUCH_PLUGIN, Messages.PluginWrapper_NoSuchPlugin(dependant));
    +                result.addDependantDisableStatus(dependantStatus);
    +
    +            // If the strategy is none and there is some enabled dependant plugin, the plugin cannot be disabled. If
    +            // this dependant plugin is not enabled, continue searching for one enabled.
    +            } else if (strategy.equals(PluginDisableStrategy.NONE)) {
    +                if (dependantPlugin.isEnabled()) {
    +                    aDependantNotDisabled = dependant;
    +                    break; // in this case, we don't need to continue with the rest of its dependants
    +                }
    +
    +            // If the strategy is not none and this dependant plugin is not enabled, add it as already disabled
    +            } else if (!dependantPlugin.isEnabled()) {
    +                PluginDisableResult dependantStatus = new PluginDisableResult(dependant, ALREADY_DISABLED, Messages.PluginWrapper_Already_Disabled(dependant));
    +                result.addDependantDisableStatus(dependantStatus);
    +
    +            // If the strategy is not none and this dependant plugin is enabled, disable it
    +            } else {
    +                // As there is no cycles in the plugin dependencies, the recursion shouldn't be infinite. The
    +                // strategy used is the same for its dependants plugins
    +                PluginDisableResult dependantResult = dependantPlugin.disable(strategy);
    +                PluginDisableStatus dependantStatus = dependantResult.status;
    +
    +                // If something wrong happened, flag this dependant plugin to set the plugin later as not-disabled due
    +                // to its dependants plugins.
    +                if (ERROR_DISABLING.equals(dependantStatus) || NOT_DISABLED_DEPENDANTS.equals(dependantStatus)) {
    +                    aDependantNotDisabled = dependant;
    +                    break; // we found a dependant plugin enabled, stop looking for dependant plugins to disable.
    +                }
    +                result.addDependantDisableStatus(dependantResult);
    +            }
    +        }
    +
    +        // If there is no enabled-dependant plugin, disable this plugin and add it to the result
    +        if (aDependantNotDisabled == null) {
    +            try {
    +                this.disableWithoutCheck();
    +                result.setMessage(Messages.PluginWrapper_Plugin_Disabled(shortName));
    +                result.setStatus(DISABLED);
    +            } catch (IOException io) {
    +                result.setMessage(Messages.PluginWrapper_Error_Disabling(shortName, io.toString()));
    +                result.setStatus(ERROR_DISABLING);
    +            }
    +        // if there is yet some not disabled dependant plugin (only possible with none strategy), this plugin cannot
    +        // be disabled.
    +        } else {
    +            result.setMessage(Messages.PluginWrapper_Plugin_Has_Dependant(shortName, aDependantNotDisabled, strategy));
    +            result.setStatus(NOT_DISABLED_DEPENDANTS);
    +        }
    +
    +        return result;
    +    }
    +
    +    private Set<String> dependantsToCheck(PluginDisableStrategy strategy) {
    +        Set<String> dependantsToCheck;
    +        switch (strategy) {
    +            case ALL:
    +                // getDependants returns all the dependant plugins, mandatory or optional.
    +                dependantsToCheck = this.getDependants();
    +                break;
    +            default:
    +                // It includes MANDATORY, NONE:
    +                // with NONE, the process only fail if mandatory dependant plugins exists
    +                // As of getDependants has all the dependants, we get the difference between them and only the optionals
    +                dependantsToCheck = Sets.difference(this.getDependants(), this.getOptionalDependants());
    +        }
    +        return dependantsToCheck;
    +    }
    +
         /**
          * Returns true if this plugin is enabled for this session.
          */
    @@ -571,7 +757,15 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
                 } else {
                     VersionNumber actualVersion = Jenkins.getVersion();
                     if (actualVersion.isOlderThan(new VersionNumber(requiredCoreVersion))) {
    -                    dependencyErrors.add(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion));
    +                    versionDependencyError(Messages.PluginWrapper_obsoleteCore(Jenkins.getVersion().toString(), requiredCoreVersion), Jenkins.getVersion().toString(), requiredCoreVersion);
    +                }
    +            }
    +
    +            String minimumJavaVersion = getMinimumJavaVersion();
    +            if (minimumJavaVersion != null) {
    +                VersionNumber actualVersion = JavaUtils.getCurrentJavaRuntimeVersionNumber();
    +                if (actualVersion.isOlderThan(new VersionNumber(minimumJavaVersion))) {
    +                    versionDependencyError(Messages.PluginWrapper_obsoleteJava(actualVersion.toString(), minimumJavaVersion), actualVersion.toString(), minimumJavaVersion);
                     }
                 }
             }
    @@ -581,21 +775,21 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
                 if (dependency == null) {
                     PluginWrapper failedDependency = NOTICE.getPlugin(d.shortName);
                     if (failedDependency != null) {
    -                    dependencyErrors.add(Messages.PluginWrapper_failed_to_load_dependency(failedDependency.getLongName(), failedDependency.getVersion()));
    +                    dependencyErrors.put(Messages.PluginWrapper_failed_to_load_dependency(failedDependency.getLongName(), failedDependency.getVersion()), true);
                         break;
                     } else {
    -                    dependencyErrors.add(Messages.PluginWrapper_missing(d.shortName, d.version));
    +                    dependencyErrors.put(Messages.PluginWrapper_missing(d.shortName, d.version), false);
                     }
                 } else {
                     if (dependency.isActive()) {
                         if (isDependencyObsolete(d, dependency)) {
    -                        dependencyErrors.add(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version));
    +                        versionDependencyError(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version);
                         }
                     } else {
                         if (isDependencyObsolete(d, dependency)) {
    -                        dependencyErrors.add(Messages.PluginWrapper_disabledAndObsolete(dependency.getLongName(), dependency.getVersion(), d.version));
    +                        versionDependencyError(Messages.PluginWrapper_disabledAndObsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version);
                         } else {
    -                        dependencyErrors.add(Messages.PluginWrapper_disabled(dependency.getLongName()));
    +                        dependencyErrors.put(Messages.PluginWrapper_disabled(dependency.getLongName()), false);
                         }
                     }
     
    @@ -606,7 +800,7 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
                 PluginWrapper dependency = parent.getPlugin(d.shortName);
                 if (dependency != null && dependency.isActive()) {
                     if (isDependencyObsolete(d, dependency)) {
    -                    dependencyErrors.add(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version));
    +                    versionDependencyError(Messages.PluginWrapper_obsolete(dependency.getLongName(), dependency.getVersion(), d.version), dependency.getVersion(), d.version);
                     } else {
                         dependencies.add(d);
                     }
    @@ -616,7 +810,7 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
                 NOTICE.addPlugin(this);
                 StringBuilder messageBuilder = new StringBuilder();
                 messageBuilder.append(Messages.PluginWrapper_failed_to_load_plugin(getLongName(), getVersion())).append(System.lineSeparator());
    -            for (Iterator<String> iterator = dependencyErrors.iterator(); iterator.hasNext(); ) {
    +            for (Iterator<String> iterator = dependencyErrors.keySet().iterator(); iterator.hasNext(); ) {
                     String dependencyError = iterator.next();
                     messageBuilder.append(" - ").append(dependencyError);
                     if (iterator.hasNext()) {
    @@ -631,6 +825,26 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
             return ENABLE_PLUGIN_DEPENDENCIES_VERSION_CHECK && dependency.getVersionNumber().isOlderThan(new VersionNumber(d.version));
         }
     
    +    /**
    +     * Called when there appears to be a core or plugin version which is too old for a stated dependency.
    +     * Normally records an error in {@link #dependencyErrors}.
    +     * But if one or both versions {@link #isSnapshot}, just issue a warning (JENKINS-52665).
    +     */
    +    private void versionDependencyError(String message, String actual, String minimum) {
    +        if (isSnapshot(actual) || isSnapshot(minimum)) {
    +            LOGGER.log(WARNING, "Suppressing dependency error in {0} v{1}: {2}", new Object[] {getLongName(), getVersion(), message});
    +        } else {
    +            dependencyErrors.put(message, false);
    +        }
    +    }
    +
    +    /**
    +     * Similar to {@code org.apache.maven.artifact.ArtifactUtils.isSnapshot}.
    +     */
    +    static boolean isSnapshot(@Nonnull String version) {
    +        return version.contains("-SNAPSHOT") || version.matches(".+-[0-9]{8}.[0-9]{6}-[0-9]+");
    +    }
    +
         /**
          * If the plugin has {@link #getUpdateInfo() an update},
          * returns the {@link hudson.model.UpdateSite.Plugin} object.
    @@ -641,7 +855,7 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
          */
         public UpdateSite.Plugin getUpdateInfo() {
             UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
    -        UpdateSite.Plugin p = uc.getPlugin(getShortName());
    +        UpdateSite.Plugin p = uc.getPlugin(getShortName(), getVersionNumber());
             if(p!=null && p.isNewerThan(getVersion())) return p;
             return null;
         }
    @@ -651,6 +865,8 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
          */
         public UpdateSite.Plugin getInfo() {
             UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
    +        UpdateSite.Plugin p = uc.getPlugin(getShortName(), getVersionNumber());
    +        if (p != null) return p;
             return uc.getPlugin(getShortName());
         }
     
    @@ -750,6 +966,11 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
                 return !plugins.isEmpty();
             }
     
    +        @Restricted(DoNotUse.class) // Jelly
    +        public boolean hasAnyDerivedDependencyErrors() {
    +            return plugins.values().stream().anyMatch(PluginWrapper::hasDerivedDependencyErrors);
    +        }
    +
             @Override
             public String getDisplayName() {
                 return Messages.PluginWrapper_PluginWrapperAdministrativeMonitor_DisplayName();
    @@ -778,6 +999,93 @@ public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
             }
         }
     
    +    /**
    +     * The result of the disablement of a plugin and its dependants plugins.
    +     */
    +    public static class PluginDisableResult {
    +        private String plugin;
    +        private PluginDisableStatus status;
    +        private String message;
    +        private Set<PluginDisableResult> dependantsDisableStatus = new HashSet<>();
    +
    +        public PluginDisableResult(String plugin) {
    +            this.plugin = plugin;
    +        }
    +
    +        public PluginDisableResult(String plugin, PluginDisableStatus status, String message) {
    +            this.plugin = plugin;
    +            this.status = status;
    +            this.message = message;
    +        }
    +
    +        public String getPlugin() {
    +            return plugin;
    +        }
    +
    +        public PluginDisableStatus getStatus() {
    +            return status;
    +        }
    +
    +        @Override
    +        public boolean equals(Object o) {
    +            if (this == o) return true;
    +            if (o == null || getClass() != o.getClass()) return false;
    +            PluginDisableResult that = (PluginDisableResult) o;
    +            return Objects.equals(plugin, that.plugin);
    +        }
    +
    +        @Override
    +        public int hashCode() {
    +            return Objects.hash(plugin);
    +        }
    +
    +        public void setStatus(PluginDisableStatus status) {
    +            this.status = status;
    +        }
    +
    +        public String getMessage() {
    +            return message;
    +        }
    +
    +        public void setMessage(String message) {
    +            this.message = message;
    +        }
    +
    +        public Set<PluginDisableResult> getDependantsDisableStatus() {
    +            return dependantsDisableStatus;
    +        }
    +
    +        public void addDependantDisableStatus(PluginDisableResult dependantDisableStatus) {
    +            dependantsDisableStatus.add(dependantDisableStatus);
    +        }
    +
    +    }
    +
    +    /**
    +     * An enum to hold the status of a disabling action against a plugin. To do it more reader-friendly.
    +     */
    +    public enum PluginDisableStatus {
    +        NO_SUCH_PLUGIN,
    +        DISABLED,
    +        ALREADY_DISABLED,
    +        NOT_DISABLED_DEPENDANTS,
    +        ERROR_DISABLING
    +    }
    +
    +    /**
    +     * The strategies defined for disabling a plugin.
    +     */
    +    public enum PluginDisableStrategy {
    +        NONE,
    +        MANDATORY,
    +        ALL;
    +
    +        @Override
    +        public String toString() {
    +            return this.name().toLowerCase();
    +        }
    +    }
    +
     //
     //
     // Action methods
    diff --git a/core/src/main/java/hudson/ProxyConfiguration.java b/core/src/main/java/hudson/ProxyConfiguration.java
    index 7c2809c53f5a2e5b208c7ad3b7b1cafaa2fc006e..11d90e17d9909cbdd6b0790a14f6ae27a930018e 100644
    --- a/core/src/main/java/hudson/ProxyConfiguration.java
    +++ b/core/src/main/java/hudson/ProxyConfiguration.java
    @@ -50,6 +50,7 @@ import java.util.List;
     import java.util.regex.Pattern;
     import javax.annotation.CheckForNull;
     import jenkins.model.Jenkins;
    +import jenkins.security.stapler.StaplerAccessibleType;
     import jenkins.util.JenkinsJVM;
     import jenkins.util.SystemProperties;
     import org.apache.commons.httpclient.Credentials;
    @@ -78,6 +79,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
      *
      * @see jenkins.model.Jenkins#proxy
      */
    +@StaplerAccessibleType
     public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfiguration> implements Saveable, Serializable {
         /**
          * Holds a default TCP connect timeout set on all connections returned from this class,
    @@ -110,6 +112,10 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
         
         private String testUrl;
     
    +    private transient Authenticator authenticator;
    +
    +    private transient boolean authCacheSeeded;
    +
         public ProxyConfiguration(String name, int port) {
             this(name,port,null,null);
         }
    @@ -129,7 +135,21 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
             this.userName = Util.fixEmptyAndTrim(userName);
             this.secretPassword = Secret.fromString(password);
             this.noProxyHost = Util.fixEmptyAndTrim(noProxyHost);
    -        this.testUrl =Util.fixEmptyAndTrim(testUrl);
    +        this.testUrl = Util.fixEmptyAndTrim(testUrl);
    +        this.authenticator = newAuthenticator();
    +    }
    +
    +    private Authenticator newAuthenticator() {
    +        return new Authenticator() {
    +            @Override
    +            public PasswordAuthentication getPasswordAuthentication() {
    +                String userName = getUserName();
    +                if (getRequestorType() == RequestorType.PROXY && userName != null) {
    +                    return new PasswordAuthentication(userName, getPassword().toCharArray());
    +                }
    +                return null;
    +            }
    +        };
         }
     
         public String getUserName() {
    @@ -204,11 +224,12 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
             SaveableListener.fireOnChange(this, config);
         }
     
    -    public Object readResolve() {
    +    private Object readResolve() {
             if (secretPassword == null)
                 // backward compatibility : get scrambled password and store it encrypted
                 secretPassword = Secret.fromString(Scrambler.descramble(password));
             password = null;
    +        authenticator = newAuthenticator();
             return this;
         }
     
    @@ -234,17 +255,12 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
             if(p==null) {
                 con = url.openConnection();
             } else {
    -            con = url.openConnection(p.createProxy(url.getHost()));
    +            Proxy proxy = p.createProxy(url.getHost());
    +            con = url.openConnection(proxy);
                 if(p.getUserName()!=null) {
                     // Add an authenticator which provides the credentials for proxy authentication
    -                Authenticator.setDefault(new Authenticator() {
    -                    @Override
    -                    public PasswordAuthentication getPasswordAuthentication() {
    -                        if (getRequestorType()!=RequestorType.PROXY)    return null;
    -                        return new PasswordAuthentication(p.getUserName(),
    -                                p.getPassword().toCharArray());
    -                    }
    -                });
    +                Authenticator.setDefault(p.authenticator);
    +                p.jenkins48775workaround(proxy, url);
                 }
             }
             
    @@ -261,27 +277,48 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
     
         public static InputStream getInputStream(URL url) throws IOException {
             final ProxyConfiguration p = get();
    -        if (p == null) 
    +        if (p == null)
                 return new RetryableHttpStream(url);
     
    -        InputStream is = new RetryableHttpStream(url, p.createProxy(url.getHost()));
    +        Proxy proxy = p.createProxy(url.getHost());
    +        InputStream is = new RetryableHttpStream(url, proxy);
             if (p.getUserName() != null) {
                 // Add an authenticator which provides the credentials for proxy authentication
    -            Authenticator.setDefault(new Authenticator() {
    -
    -                @Override
    -                public PasswordAuthentication getPasswordAuthentication() {
    -                    if (getRequestorType() != RequestorType.PROXY) {
    -                        return null;
    -                    }
    -                    return new PasswordAuthentication(p.getUserName(), p.getPassword().toCharArray());
    -                }
    -            });
    +            Authenticator.setDefault(p.authenticator);
    +            p.jenkins48775workaround(proxy, url);
             }
     
             return is;
         }
     
    +    /**
    +     * If the first URL we try to access with a HTTP proxy is HTTPS then the authentication cache will not have been
    +     * pre-populated, so we try to access at least one HTTP URL before the very first HTTPS url.
    +     * @param proxy
    +     * @param url the actual URL being opened.
    +     */
    +    private void jenkins48775workaround(Proxy proxy, URL url) {
    +        if ("https".equals(url.getProtocol()) && !authCacheSeeded && proxy != Proxy.NO_PROXY) {
    +            HttpURLConnection preAuth = null;
    +            try {
    +                // We do not care if there is anything at this URL, all we care is that it is using the proxy
    +                preAuth = (HttpURLConnection) new URL("http", url.getHost(), -1, "/").openConnection(proxy);
    +                preAuth.setRequestMethod("HEAD");
    +                preAuth.connect();
    +            } catch (IOException e) {
    +                // ignore, this is just a probe we don't care at all
    +            } finally {
    +                if (preAuth != null) {
    +                    preAuth.disconnect();
    +                }
    +            }
    +            authCacheSeeded = true;
    +        } else if ("https".equals(url.getProtocol())){
    +            // if we access any http url using a proxy then the auth cache will have been seeded
    +            authCacheSeeded = authCacheSeeded || proxy != Proxy.NO_PROXY;
    +        }
    +    }
    +
         @CheckForNull
         private static ProxyConfiguration get() {
             if (JenkinsJVM.isJenkinsJVM()) {
    @@ -341,6 +378,8 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
                     @QueryParameter("userName") String userName, @QueryParameter("password") String password,
                     @QueryParameter("noProxyHost") String noProxyHost) {
     
    +            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +
                 if (Util.fixEmptyAndTrim(testUrl) == null) {
                     return FormValidation.error(Messages.ProxyConfiguration_TestUrlRequired());
                 }
    @@ -396,7 +435,7 @@ public final class ProxyConfiguration extends AbstractDescribableImpl<ProxyConfi
                 if (userName.indexOf('\\') >= 0){
                     final String domain = userName.substring(0, userName.indexOf('\\'));
                     final String user = userName.substring(userName.indexOf('\\') + 1);
    -                return new NTCredentials(user, Secret.fromString(password).getPlainText(), domain, "");
    +                return new NTCredentials(user, Secret.fromString(password).getPlainText(), "", domain);
                 } else {
                     return new UsernamePasswordCredentials(userName, Secret.fromString(password).getPlainText());
                 }
    diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java
    index bae4e2b4a90bec58b1aa4a3fae2cd1150af3e507..40a3fc5d41ec5fedb4d95dd9d454003e67d3e93a 100644
    --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java
    +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java
    @@ -30,8 +30,12 @@ import java.io.Writer;
     import java.nio.charset.Charset;
     import java.security.interfaces.RSAPublicKey;
     import javax.annotation.Nullable;
    +
    +import hudson.model.AperiodicWork;
     import jenkins.model.Jenkins;
     import jenkins.model.identity.InstanceIdentityProvider;
    +import jenkins.security.stapler.StaplerAccessibleType;
    +import jenkins.slaves.RemotingVersionInfo;
     import jenkins.util.SystemProperties;
     import hudson.slaves.OfflineCause;
     import java.io.DataOutputStream;
    @@ -53,16 +57,18 @@ import java.net.Socket;
     import java.nio.channels.ServerSocketChannel;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +
     import org.apache.commons.codec.binary.Base64;
     import org.apache.commons.io.Charsets;
     import org.apache.commons.io.IOUtils;
     import org.apache.commons.io.output.NullOutputStream;
     import org.apache.commons.lang.StringUtils;
    +import org.jenkinsci.Symbol;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
    - * Listens to incoming TCP connections from JNLP agents and Remoting CLI.
    + * Listens to incoming TCP connections from JNLP agents and deprecated Remoting-based CLI.
      *
      * <p>
      * Aside from the HTTP endpoint, Jenkins runs {@link TcpSlaveAgentListener} that listens on a TCP socket.
    @@ -77,6 +83,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
      * @author Kohsuke Kawaguchi
      * @see AgentProtocol
      */
    +@StaplerAccessibleType
     public final class TcpSlaveAgentListener extends Thread {
     
         private final ServerSocketChannel serverSocket;
    @@ -97,6 +104,11 @@ public final class TcpSlaveAgentListener extends Thread {
                 throw (BindException)new BindException("Failed to listen on port "+port+" because it's already in use.").initCause(e);
             }
             this.configuredPort = port;
    +        setUncaughtExceptionHandler((t, e) -> {
    +            LOGGER.log(Level.SEVERE, "Uncaught exception in TcpSlaveAgentListener " + t + ", attempting to reschedule thread", e);
    +            shutdown();
    +            TcpSlaveAgentListenerRescheduler.schedule(t, e);
    +        });
     
             LOGGER.log(Level.FINE, "TCP agent listener started on port {0}", getPort());
     
    @@ -154,7 +166,14 @@ public final class TcpSlaveAgentListener extends Thread {
                     // we take care of buffering on our own
                     s.setTcpNoDelay(true);
     
    -                new ConnectionHandler(s).start();
    +                new ConnectionHandler(s, new ConnectionHandlerFailureCallback(this) {
    +                    @Override
    +                    public void run(Throwable cause) {
    +                        LOGGER.log(Level.WARNING, "Connection handler failed, restarting listener", cause);
    +                        shutdown();
    +                        TcpSlaveAgentListenerRescheduler.schedule(getParentThread(), cause);
    +                    }
    +                }).start();
                 }
             } catch (IOException e) {
                 if(!shuttingDown) {
    @@ -193,12 +212,21 @@ public final class TcpSlaveAgentListener extends Thread {
              */
             private final int id;
     
    -        public ConnectionHandler(Socket s) {
    +        public ConnectionHandler(Socket s, ConnectionHandlerFailureCallback parentTerminator) {
                 this.s = s;
                 synchronized(getClass()) {
                     id = iotaGen++;
                 }
                 setName("TCP agent connection handler #"+id+" with "+s.getRemoteSocketAddress());
    +            setUncaughtExceptionHandler((t, e) -> {
    +                LOGGER.log(Level.SEVERE, "Uncaught exception in TcpSlaveAgentListener ConnectionHandler " + t, e);
    +                try {
    +                    s.close();
    +                    parentTerminator.run(e);
    +                } catch (IOException e1) {
    +                    LOGGER.log(Level.WARNING, "Could not close socket after unexpected thread death", e1);
    +                }
    +            });
             }
     
             @Override
    @@ -265,6 +293,7 @@ public final class TcpSlaveAgentListener extends Thread {
                 try {
                     Writer o = new OutputStreamWriter(s.getOutputStream(), "UTF-8");
     
    +                //TODO: expose version about minimum supported Remoting version (JENKINS-48766)
                     if (header.startsWith("GET / ")) {
                         o.write("HTTP/1.0 200 OK\r\n");
                         o.write("Content-Type: text/plain;charset=UTF-8\r\n");
    @@ -274,6 +303,7 @@ public final class TcpSlaveAgentListener extends Thread {
                         o.write("Jenkins-Session: " + Jenkins.SESSION_HASH + "\r\n");
                         o.write("Client: " + s.getInetAddress().getHostAddress() + "\r\n");
                         o.write("Server: " + s.getLocalAddress().getHostAddress() + "\r\n");
    +                    o.write("Remoting-Minimum-Version: " + RemotingVersionInfo.getMinimumSupportedVersion() + "\r\n");
                         o.flush();
                         s.shutdownOutput();
                     } else {
    @@ -300,6 +330,21 @@ public final class TcpSlaveAgentListener extends Thread {
             }
         }
     
    +    // This is essentially just to be able to pass the parent thread into the callback, as it can't access it otherwise
    +    private abstract class ConnectionHandlerFailureCallback {
    +        private Thread parentThread;
    +
    +        public ConnectionHandlerFailureCallback(Thread parentThread) {
    +            this.parentThread = parentThread;
    +        }
    +
    +        public Thread getParentThread() {
    +            return parentThread;
    +        }
    +
    +        public abstract void run(Throwable cause);
    +    }
    +
         /**
          * This extension provides a Ping protocol that allows people to verify that the TcpSlaveAgentListener is alive.
          * We also use this to wake the acceptor thread on termination.
    @@ -307,6 +352,7 @@ public final class TcpSlaveAgentListener extends Thread {
          * @since 1.653
          */
         @Extension
    +    @Symbol("ping")
         public static class PingAgentProtocol extends AgentProtocol {
     
             private final byte[] ping;
    @@ -369,7 +415,9 @@ public final class TcpSlaveAgentListener extends Thread {
                                 LOGGER.log(Level.FINE, "Expected ping response from {0} of {1} got {2}", new Object[]{
                                         socket.getRemoteSocketAddress(),
                                         new String(ping, "UTF-8"),
    -                                    new String(response, 0, responseLength, "UTF-8")
    +                                    responseLength > 0 && responseLength <= response.length ?
    +                                        new String(response, 0, responseLength, "UTF-8") :
    +                                        "bad response length " + responseLength
                                 });
                                 return false;
                             }
    @@ -381,6 +429,84 @@ public final class TcpSlaveAgentListener extends Thread {
             }
         }
     
    +    /**
    +     * Reschedules the <code>TcpSlaveAgentListener</code> on demand.  Disables itself after running.
    +     */
    +    @Extension
    +    @Restricted(NoExternalUse.class)
    +    public static class TcpSlaveAgentListenerRescheduler extends AperiodicWork {
    +        private Thread originThread;
    +        private Throwable cause;
    +        private long recurrencePeriod = 5000;
    +        private boolean isActive;
    +
    +        public TcpSlaveAgentListenerRescheduler() {
    +            isActive = false;
    +        }
    +
    +        public TcpSlaveAgentListenerRescheduler(Thread originThread, Throwable cause) {
    +            this.originThread = originThread;
    +            this.cause = cause;
    +            this.isActive = false;
    +        }
    +
    +        public void setOriginThread(Thread originThread) {
    +            this.originThread = originThread;
    +        }
    +
    +        public void setCause(Throwable cause) {
    +            this.cause = cause;
    +        }
    +
    +        public void setActive(boolean active) {
    +            isActive = active;
    +        }
    +
    +        @Override
    +        public long getRecurrencePeriod() {
    +            return recurrencePeriod;
    +        }
    +
    +        @Override
    +        public AperiodicWork getNewInstance() {
    +            return new TcpSlaveAgentListenerRescheduler(originThread, cause);
    +        }
    +
    +        @Override
    +        protected void doAperiodicRun() {
    +            if (isActive) {
    +                try {
    +                    if (originThread.isAlive()) {
    +                        originThread.interrupt();
    +                    }
    +                    int port = Jenkins.getInstance().getSlaveAgentPort();
    +                    if (port != -1) {
    +                        new TcpSlaveAgentListener(port).start();
    +                        LOGGER.log(Level.INFO, "Restarted TcpSlaveAgentListener");
    +                    } else {
    +                        LOGGER.log(Level.SEVERE, "Uncaught exception in TcpSlaveAgentListener " + originThread + ". Port is disabled, not rescheduling", cause);
    +                    }
    +                    isActive = false;
    +                } catch (IOException e) {
    +                    LOGGER.log(Level.SEVERE, "Could not reschedule TcpSlaveAgentListener - trying again.", cause);
    +                }
    +            }
    +        }
    +
    +        public static void schedule(Thread originThread, Throwable cause) {
    +            schedule(originThread, cause,5000);
    +        }
    +
    +        public static void schedule(Thread originThread, Throwable cause, long approxDelay) {
    +            TcpSlaveAgentListenerRescheduler rescheduler = AperiodicWork.all().get(TcpSlaveAgentListenerRescheduler.class);
    +            rescheduler.originThread = originThread;
    +            rescheduler.cause = cause;
    +            rescheduler.recurrencePeriod = approxDelay;
    +            rescheduler.isActive = true;
    +        }
    +    }
    +
    +
         /**
          * Connection terminated because we are reconnected from the current peer.
          */
    @@ -395,10 +521,10 @@ public final class TcpSlaveAgentListener extends Thread {
         private static final Logger LOGGER = Logger.getLogger(TcpSlaveAgentListener.class.getName());
     
         /**
    -     * Host name that we advertise the CLI client to connect to.
    +     * Host name that we advertise protocol clients to connect to.
          * This is primarily for those who have reverse proxies in place such that the HTTP host name
    -     * and the CLI TCP/IP connection host names are different.
    -     *
    +     * and the TCP/IP connection host names are different.
    +     * (Note: despite the name, this is used for any client, not only deprecated Remoting-based CLI.)
          * TODO: think about how to expose this (including whether this needs to be exposed at all.)
          */
         @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
    @@ -406,11 +532,11 @@ public final class TcpSlaveAgentListener extends Thread {
         public static String CLI_HOST_NAME = SystemProperties.getString(TcpSlaveAgentListener.class.getName()+".hostName");
     
         /**
    -     * Port number that we advertise the CLI client to connect to.
    +     * Port number that we advertise protocol clients to connect to.
          * This is primarily for the case where the port that Jenkins runs is different from the port
          * that external world should connect to, because of the presence of NAT / port-forwarding / TCP reverse
          * proxy.
    -     *
    +     * (Note: despite the name, this is used for any client, not only deprecated Remoting-based CLI.)
          * If left to null, fall back to {@link #getPort()}
          *
          * @since 1.611
    diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java
    index 07ff96fb007f9939cddc122f56ef8351c33c321b..2aeeb74bd57f23bef2379da30962c151369b4031 100644
    --- a/core/src/main/java/hudson/Util.java
    +++ b/core/src/main/java/hudson/Util.java
    @@ -23,35 +23,25 @@
      */
     package hudson;
     
    -import java.nio.file.InvalidPathException;
    -import jenkins.util.SystemProperties;
    -import com.sun.jna.Native;
    -
    -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    -import hudson.Proc.LocalProc;
     import hudson.model.TaskListener;
    -import hudson.os.PosixAPI;
    +import jenkins.util.MemoryReductionUtil;
     import hudson.util.QuotedStringTokenizer;
     import hudson.util.VariableResolver;
    -import hudson.util.jna.WinIOException;
    +import jenkins.util.SystemProperties;
     
    +import jenkins.util.io.PathRemover;
    +import org.apache.commons.codec.digest.DigestUtils;
     import org.apache.commons.io.IOUtils;
    +import org.apache.commons.io.output.NullOutputStream;
     import org.apache.commons.lang.time.FastDateFormat;
     import org.apache.tools.ant.BuildException;
     import org.apache.tools.ant.Project;
    -import org.apache.tools.ant.taskdefs.Chmod;
     import org.apache.tools.ant.taskdefs.Copy;
     import org.apache.tools.ant.types.FileSet;
     
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     
    -import jnr.posix.FileStat;
    -import jnr.posix.POSIX;
    -
    -import javax.crypto.SecretKey;
    -import javax.crypto.spec.SecretKeySpec;
    -
     import java.io.*;
     import java.lang.reflect.Method;
     import java.lang.reflect.Modifier;
    @@ -64,33 +54,45 @@ import java.nio.CharBuffer;
     import java.nio.charset.CharacterCodingException;
     import java.nio.charset.Charset;
     import java.nio.charset.CharsetEncoder;
    +import java.nio.charset.StandardCharsets;
     import java.nio.file.FileAlreadyExistsException;
     import java.nio.file.FileSystemException;
    +import java.nio.file.FileSystems;
     import java.nio.file.Files;
    +import java.nio.file.InvalidPathException;
    +import java.nio.file.LinkOption;
     import java.nio.file.Path;
     import java.nio.file.Paths;
    +import java.nio.file.attribute.BasicFileAttributes;
    +import java.nio.file.attribute.DosFileAttributes;
    +import java.nio.file.attribute.PosixFilePermission;
    +import java.nio.file.attribute.PosixFilePermissions;
    +import java.security.DigestInputStream;
     import java.security.MessageDigest;
     import java.security.NoSuchAlgorithmException;
     import java.text.NumberFormat;
     import java.text.ParseException;
    +import java.time.LocalDate;
    +import java.time.ZoneId;
    +import java.time.temporal.ChronoUnit;
     import java.util.*;
     import java.util.concurrent.TimeUnit;
     import java.util.concurrent.atomic.AtomicBoolean;
     import java.util.logging.Level;
    +import java.util.logging.LogRecord;
     import java.util.logging.Logger;
     import java.util.regex.Matcher;
     import java.util.regex.Pattern;
     
    -import hudson.util.jna.Kernel32Utils;
    -import static hudson.util.jna.GNUCLibrary.LIBC;
    -
    -import java.security.DigestInputStream;
    -
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import javax.annotation.Nullable;
    +import javax.crypto.SecretKey;
    +import javax.crypto.spec.SecretKeySpec;
     
    -import org.apache.commons.codec.digest.DigestUtils;
    +import org.apache.commons.io.FileUtils;
    +import org.kohsuke.stapler.Ancestor;
    +import org.kohsuke.stapler.StaplerRequest;
     
     /**
      * Various utility methods that don't have more proper home.
    @@ -135,7 +137,7 @@ public class Util {
         private static final Pattern VARIABLE = Pattern.compile("\\$([A-Za-z0-9_]+|\\{[A-Za-z0-9_.]+\\}|\\$)");
     
         /**
    -     * Replaces the occurrence of '$key' by <tt>properties.get('key')</tt>.
    +     * Replaces the occurrence of '$key' by {@code properties.get('key')}.
          *
          * <p>
          * Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.)
    @@ -147,7 +149,7 @@ public class Util {
         }
     
         /**
    -     * Replaces the occurrence of '$key' by <tt>resolver.get('key')</tt>.
    +     * Replaces the occurrence of '$key' by {@code resolver.get('key')}.
          *
          * <p>
          * Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.)
    @@ -184,30 +186,54 @@ public class Util {
         }
     
         /**
    -     * Loads the contents of a file into a string.
    +     * Reads the entire contents of the text file at <code>logfile</code> into a
    +     * string using the {@link Charset#defaultCharset() default charset} for
    +     * decoding. If no such file exists, an empty string is returned.
    +     * @param logfile The text file to read in its entirety.
    +     * @return The entire text content of <code>logfile</code>.
    +     * @throws IOException If an error occurs while reading the file.
    +     * @deprecated call {@link #loadFile(java.io.File, java.nio.charset.Charset)}
    +     * instead to specify the charset to use for decoding (preferably
    +     * {@link java.nio.charset.StandardCharsets#UTF_8}).
          */
         @Nonnull
    +    @Deprecated
         public static String loadFile(@Nonnull File logfile) throws IOException {
             return loadFile(logfile, Charset.defaultCharset());
         }
     
    +    /**
    +     * Reads the entire contents of the text file at <code>logfile</code> into a
    +     * string using <code>charset</code> for decoding. If no such file exists,
    +     * an empty string is returned.
    +     * @param logfile The text file to read in its entirety.
    +     * @param charset The charset to use for decoding the bytes in <code>logfile</code>.
    +     * @return The entire text content of <code>logfile</code>.
    +     * @throws IOException If an error occurs while reading the file.
    +     */
         @Nonnull
         public static String loadFile(@Nonnull File logfile, @Nonnull Charset charset) throws IOException {
    -        if(!logfile.exists())
    +        // Note: Until charset handling is resolved (e.g. by implementing
    +        // https://issues.jenkins-ci.org/browse/JENKINS-48923 ), this method
    +        // must be able to handle character encoding errors. As reported at
    +        // https://issues.jenkins-ci.org/browse/JENKINS-49112 Run.getLog() calls
    +        // loadFile() to fully read the generated log file. This file might
    +        // contain unmappable and/or malformed byte sequences. We need to make
    +        // sure that in such cases, no CharacterCodingException is thrown.
    +        //
    +        // One approach that cannot be used is to call Files.newBufferedReader()
    +        // because there is a difference in how an InputStreamReader constructed
    +        // from a Charset and the reader returned by Files.newBufferedReader()
    +        // handle malformed and unmappable byte sequences for the specified
    +        // encoding; the latter is more picky and will throw an exception.
    +        // See: https://issues.jenkins-ci.org/browse/JENKINS-49060?focusedCommentId=325989&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-325989
    +        try {
    +            return FileUtils.readFileToString(logfile, charset);
    +        } catch (FileNotFoundException e) {
                 return "";
    -
    -        StringBuilder str = new StringBuilder((int)logfile.length());
    -
    -        try (BufferedReader r = new BufferedReader(new InputStreamReader(Files.newInputStream(logfile.toPath()), charset))) {
    -            char[] buf = new char[1024];
    -            int len;
    -            while ((len = r.read(buf, 0, buf.length)) > 0)
    -                str.append(buf, 0, len);
    -        } catch (InvalidPathException e) {
    -            throw new IOException(e);
    +        } catch (Exception e) {
    +            throw new IOException("Failed to fully read " + logfile, e);
             }
    -
    -        return str.toString();
         }
     
         /**
    @@ -220,16 +246,18 @@ public class Util {
          *      if the operation fails.
          */
         public static void deleteContentsRecursive(@Nonnull File file) throws IOException {
    -        for( int numberOfAttempts=1 ; ; numberOfAttempts++ ) {
    -            try {
    -                tryOnceDeleteContentsRecursive(file);
    -                break; // success
    -            } catch (IOException ex) {
    -                boolean threadWasInterrupted = pauseBetweenDeletes(numberOfAttempts);
    -                if( numberOfAttempts>= DELETION_MAX || threadWasInterrupted)
    -                    throw new IOException(deleteFailExceptionMessage(file, numberOfAttempts, threadWasInterrupted), ex);
    -            }
    -        }
    +        deleteContentsRecursive(fileToPath(file), PathRemover.PathChecker.ALLOW_ALL);
    +    }
    +
    +    /**
    +     * Deletes the given directory contents (but not the directory itself) recursively using a PathChecker.
    +     * @param path a directory to delete
    +     * @param pathChecker a security check to validate a path before deleting
    +     * @throws IOException if the operation fails
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static void deleteContentsRecursive(@Nonnull Path path, @Nonnull PathRemover.PathChecker pathChecker) throws IOException {
    +        newPathRemover(pathChecker).forceRemoveDirectoryContents(path);
         }
     
         /**
    @@ -240,97 +268,7 @@ public class Util {
          * @throws IOException if it exists but could not be successfully deleted
          */
         public static void deleteFile(@Nonnull File f) throws IOException {
    -        for( int numberOfAttempts=1 ; ; numberOfAttempts++ ) {
    -            try {
    -                tryOnceDeleteFile(f);
    -                break; // success
    -            } catch (IOException ex) {
    -                boolean threadWasInterrupted = pauseBetweenDeletes(numberOfAttempts);
    -                if( numberOfAttempts>= DELETION_MAX || threadWasInterrupted)
    -                    throw new IOException(deleteFailExceptionMessage(f, numberOfAttempts, threadWasInterrupted), ex);
    -            }
    -        }
    -    }
    -
    -    /**
    -     * Deletes this file, working around most problems which might make
    -     * this difficult.
    -     * 
    -     * @param f
    -     *            What to delete. If a directory, it'll need to be empty.
    -     * @throws IOException if it exists but could not be successfully deleted
    -     */
    -    private static void tryOnceDeleteFile(File f) throws IOException {
    -        if (!f.delete()) {
    -            if(!f.exists())
    -                // we are trying to delete a file that no longer exists, so this is not an error
    -                return;
    -
    -            // perhaps this file is read-only?
    -            makeWritable(f);
    -            /*
    -             on Unix both the file and the directory that contains it has to be writable
    -             for a file deletion to be successful. (Confirmed on Solaris 9)
    -
    -             $ ls -la
    -             total 6
    -             dr-xr-sr-x   2 hudson   hudson       512 Apr 18 14:41 .
    -             dr-xr-sr-x   3 hudson   hudson       512 Apr 17 19:36 ..
    -             -r--r--r--   1 hudson   hudson       469 Apr 17 19:36 manager.xml
    -             -rw-r--r--   1 hudson   hudson         0 Apr 18 14:41 x
    -             $ rm x
    -             rm: x not removed: Permission denied
    -             */
    -
    -            makeWritable(f.getParentFile());
    -
    -            if(!f.delete() && f.exists()) {
    -                // trouble-shooting.
    -                try {
    -                    Files.deleteIfExists(f.toPath());
    -                } catch (InvalidPathException e) {
    -                    throw new IOException(e);
    -                }
    -
    -                // see https://java.net/projects/hudson/lists/users/archive/2008-05/message/357
    -                // I suspect other processes putting files in this directory
    -                File[] files = f.listFiles();
    -                if(files!=null && files.length>0)
    -                    throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files));
    -                throw new IOException("Unable to delete " + f.getPath());
    -            }
    -        }
    -    }
    -
    -    /**
    -     * Makes the given file writable by any means possible.
    -     */
    -    private static void makeWritable(@Nonnull File f) {
    -        if (f.setWritable(true)) {
    -            return;
    -        }
    -        // TODO do we still need to try anything else?
    -
    -        // try chmod. this becomes no-op if this is not Unix.
    -        try {
    -            Chmod chmod = new Chmod();
    -            chmod.setProject(new Project());
    -            chmod.setFile(f);
    -            chmod.setPerm("u+w");
    -            chmod.execute();
    -        } catch (BuildException e) {
    -            LOGGER.log(Level.INFO,"Failed to chmod "+f,e);
    -        }
    -
    -        try {// try libc chmod
    -            POSIX posix = PosixAPI.jnr();
    -            String path = f.getAbsolutePath();
    -            FileStat stat = posix.stat(path);
    -            posix.chmod(path, stat.mode()|0200); // u+w
    -        } catch (Throwable t) {
    -            LOGGER.log(Level.FINE,"Failed to chmod(2) "+f,t);
    -        }
    -
    +        newPathRemover(PathRemover.PathChecker.ALLOW_ALL).forceRemoveFile(fileToPath(f));
         }
     
         /**
    @@ -342,137 +280,18 @@ public class Util {
          * if the operation fails.
          */
         public static void deleteRecursive(@Nonnull File dir) throws IOException {
    -        for( int numberOfAttempts=1 ; ; numberOfAttempts++ ) {
    -            try {
    -                tryOnceDeleteRecursive(dir);
    -                break; // success
    -            } catch (IOException ex) {
    -                boolean threadWasInterrupted = pauseBetweenDeletes(numberOfAttempts);
    -                if( numberOfAttempts>= DELETION_MAX || threadWasInterrupted)
    -                    throw new IOException(deleteFailExceptionMessage(dir, numberOfAttempts, threadWasInterrupted), ex);
    -            }
    -        }
    +        deleteRecursive(fileToPath(dir), PathRemover.PathChecker.ALLOW_ALL);
         }
     
         /**
    -     * Deletes a file or folder, throwing the first exception encountered, but
    -     * having a go at deleting everything. i.e. it does not <em>stop</em> on the
    -     * first exception, but tries (to delete) everything once.
    -     *
    -     * @param dir
    -     * What to delete. If a directory, the contents will be deleted
    -     * too.
    -     * @throws The first exception encountered.
    +     * Deletes the given directory and contents recursively using a filter.
    +     * @param dir a directory to delete
    +     * @param pathChecker a security check to validate a path before deleting
    +     * @throws IOException if the operation fails
          */
    -    private static void tryOnceDeleteRecursive(File dir) throws IOException {
    -        if(!isSymlink(dir))
    -            tryOnceDeleteContentsRecursive(dir);
    -        tryOnceDeleteFile(dir);
    -    }
    -
    -    /**
    -     * Deletes a folder's contents, throwing the first exception encountered,
    -     * but having a go at deleting everything. i.e. it does not <em>stop</em>
    -     * on the first exception, but tries (to delete) everything once.
    -     *
    -     * @param directory
    -     * The directory whose contents will be deleted.
    -     * @throws The first exception encountered.
    -     */
    -    private static void tryOnceDeleteContentsRecursive(File directory) throws IOException {
    -        File[] directoryContents = directory.listFiles();
    -        if(directoryContents==null)
    -            return; // the directory didn't exist in the first place
    -        IOException firstCaught = null;
    -        for (File child : directoryContents) {
    -            try {
    -                tryOnceDeleteRecursive(child);
    -            } catch (IOException justCaught) {
    -                if( firstCaught==null) {
    -                    firstCaught = justCaught;
    -                }
    -            }
    -        }
    -        if( firstCaught!=null )
    -            throw firstCaught;
    -    }
    -
    -    /**
    -     * Pauses between delete attempts, and says if it's ok to try again.
    -     * This does not wait if the wait time is zero or if we have tried
    -     * too many times already.
    -     * <p>
    -     * See {@link #WAIT_BETWEEN_DELETION_RETRIES} for details of
    -     * the pause duration.<br/>
    -     * See {@link #GC_AFTER_FAILED_DELETE} for when {@link System#gc()} is called.
    -     * 
    -     * @return false if it is ok to continue trying to delete things, true if
    -     *         we were interrupted (and should stop now).
    -     */
    -    @SuppressFBWarnings(value = "DM_GC", justification = "Garbage collection happens only when "
    -            + "GC_AFTER_FAILED_DELETE is true. It's an experimental feature in Jenkins.")
    -    private static boolean pauseBetweenDeletes(int numberOfAttemptsSoFar) {
    -        long delayInMs;
    -        if( numberOfAttemptsSoFar>=DELETION_MAX ) return false;
    -        /* If the Jenkins process had the file open earlier, and it has not
    -         * closed it then Windows won't let us delete it until the Java object
    -         * with the open stream is Garbage Collected, which can result in builds
    -         * failing due to "file in use" on Windows despite working perfectly
    -         * well on other OSs. */
    -        if (GC_AFTER_FAILED_DELETE) {
    -            System.gc();
    -        }
    -        if (WAIT_BETWEEN_DELETION_RETRIES>=0) {
    -            delayInMs = WAIT_BETWEEN_DELETION_RETRIES;
    -        } else {
    -            delayInMs = -numberOfAttemptsSoFar*WAIT_BETWEEN_DELETION_RETRIES;
    -        }
    -        if (delayInMs<=0)
    -            return Thread.interrupted();
    -        try {
    -            Thread.sleep(delayInMs);
    -            return false;
    -        } catch (InterruptedException e) {
    -            return true;
    -        }
    -    }
    -
    -    /**
    -     * Creates a "couldn't delete file" message that explains how hard we tried.
    -     * See {@link #DELETION_MAX}, {@link #WAIT_BETWEEN_DELETION_RETRIES}
    -     * and {@link #GC_AFTER_FAILED_DELETE} for more details.
    -     */
    -    private static String deleteFailExceptionMessage(File whatWeWereTryingToRemove, int retryCount, boolean wasInterrupted) {
    -        StringBuilder sb = new StringBuilder();
    -        sb.append("Unable to delete '");
    -        sb.append(whatWeWereTryingToRemove);
    -        sb.append("'. Tried ");
    -        sb.append(retryCount);
    -        sb.append(" time");
    -        if( retryCount!=1 ) sb.append('s');
    -        if( DELETION_MAX>1 ) {
    -            sb.append(" (of a maximum of ");
    -            sb.append(DELETION_MAX);
    -            sb.append(')');
    -            if( GC_AFTER_FAILED_DELETE )
    -                sb.append(" garbage-collecting");
    -            if( WAIT_BETWEEN_DELETION_RETRIES!=0 && GC_AFTER_FAILED_DELETE )
    -                sb.append(" and");
    -            if( WAIT_BETWEEN_DELETION_RETRIES!=0 ) {
    -                sb.append(" waiting ");
    -                sb.append(getTimeSpanString(Math.abs(WAIT_BETWEEN_DELETION_RETRIES)));
    -                if( WAIT_BETWEEN_DELETION_RETRIES<0 ) {
    -                    sb.append("-");
    -                    sb.append(getTimeSpanString(Math.abs(WAIT_BETWEEN_DELETION_RETRIES)*DELETION_MAX));
    -                }
    -            }
    -            if( WAIT_BETWEEN_DELETION_RETRIES!=0 || GC_AFTER_FAILED_DELETE)
    -                sb.append(" between attempts");
    -        }
    -        if( wasInterrupted )
    -            sb.append(". The delete operation was interrupted before it completed successfully");
    -        sb.append('.');
    -        return sb.toString();
    +    @Restricted(NoExternalUse.class)
    +    public static void deleteRecursive(@Nonnull Path dir, @Nonnull PathRemover.PathChecker pathChecker) throws IOException {
    +        newPathRemover(pathChecker).forceRemoveRecursive(dir);
         }
     
         /*
    @@ -491,10 +310,16 @@ public class Util {
          * limitations under the License.
          */
         /**
    -     * Checks if the given file represents a symlink.
    +     * Checks if the given file represents a symlink. Unlike {@link Files#isSymbolicLink(Path)}, this method also
    +     * considers <a href="https://en.wikipedia.org/wiki/NTFS_junction_point">NTFS junction points</a> as symbolic
    +     * links.
          */
    -    //Taken from http://svn.apache.org/viewvc/maven/shared/trunk/file-management/src/main/java/org/apache/maven/shared/model/fileset/util/FileSetManager.java?view=markup
         public static boolean isSymlink(@Nonnull File file) throws IOException {
    +        return isSymlink(fileToPath(file));
    +    }
    +
    +    @Restricted(NoExternalUse.class)
    +    public static boolean isSymlink(@Nonnull Path path) {
             /*
              *  Windows Directory Junctions are effectively the same as Linux symlinks to directories.
              *  Unfortunately, the Java 7 NIO2 API function isSymbolicLink does not treat them as such.
    @@ -502,44 +327,16 @@ public class Util {
              *  you have to go through BasicFileAttributes and do the following check:
              *     isSymbolicLink() || isOther()
              *  The isOther() call will include Windows reparse points, of which a directory junction is.
    -         *
    -         *  Since we already have a function that detects Windows junctions or symlinks and treats them
    -         *  both as symlinks, let's use that function and always call it before calling down to the
    -         *  NIO2 API.
    -         *
    +         *  It also includes includes devices, but reading the attributes of a device with NIO fails
    +         *  or returns false for isOther(). (i.e. named pipes such as \\.\pipe\JenkinsTestPipe return
    +         *  false for isOther(), and drives such as \\.\PhysicalDrive0 throw an exception when
    +         *  calling readAttributes.
              */
    -        if (Functions.isWindows()) {
    -            try {
    -                return Kernel32Utils.isJunctionOrSymlink(file);
    -            } catch (UnsupportedOperationException | LinkageError e) {
    -                // fall through
    -            }
    -        }
    -        Boolean r = isSymlinkJava7(file);
    -        if (r != null) {
    -            return r;
    -        }
    -        String name = file.getName();
    -        if (name.equals(".") || name.equals(".."))
    -            return false;
    -
    -        File fileInCanonicalParent;
    -        File parentDir = file.getParentFile();
    -        if ( parentDir == null ) {
    -            fileInCanonicalParent = file;
    -        } else {
    -            fileInCanonicalParent = new File( parentDir.getCanonicalPath(), name );
    -        }
    -        return !fileInCanonicalParent.getCanonicalFile().equals( fileInCanonicalParent.getAbsoluteFile() );
    -    }
    -
    -    @SuppressFBWarnings("NP_BOOLEAN_RETURN_NULL")
    -    private static Boolean isSymlinkJava7(@Nonnull File file) throws IOException {
             try {
    -            Path path = file.toPath();
    -            return Files.isSymbolicLink(path);
    -        } catch (Exception x) {
    -            throw (IOException) new IOException(x.toString()).initCause(x);
    +            BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    +            return attrs.isSymbolicLink() || (attrs instanceof DosFileAttributes && attrs.isOther());
    +        } catch (IOException ignored) {
    +            return false;
             }
         }
     
    @@ -567,16 +364,45 @@ public class Util {
             return true;
         }
     
    +    /**
    +     * A check if a file path is a descendant of a parent path
    +     * @param forParent the parent the child should be a descendant of
    +     * @param potentialChild the path to check
    +     * @return true if so
    +     * @throws IOException for invalid paths
    +     * @since 2.80
    +     * @see InvalidPathException
    +     */
    +    public static boolean isDescendant(File forParent, File potentialChild) throws IOException {
    +        Path child = fileToPath(potentialChild.getAbsoluteFile()).normalize();
    +        Path parent = fileToPath(forParent.getAbsoluteFile()).normalize();
    +        return child.startsWith(parent);
    +    }
    +
         /**
          * Creates a new temporary directory.
          */
         public static File createTempDir() throws IOException {
    -        File tmp = File.createTempFile("jenkins", "tmp");
    -        if(!tmp.delete())
    -            throw new IOException("Failed to delete "+tmp);
    -        if(!tmp.mkdirs())
    -            throw new IOException("Failed to create a new directory "+tmp);
    -        return tmp;
    +        // The previously used approach of creating a temporary file, deleting
    +        // it, and making a new directory having the same name in its place is
    +        // potentially  problematic:
    +        // https://stackoverflow.com/questions/617414/how-to-create-a-temporary-directory-folder-in-java
    +        // We can use the Java 7 Files.createTempDirectory() API, but note that
    +        // by default, the permissions of the created directory are 0700&(~umask)
    +        // whereas the old approach created a temporary directory with permissions
    +        // 0777&(~umask).
    +        // To avoid permissions problems like https://issues.jenkins-ci.org/browse/JENKINS-48407
    +        // we can pass POSIX file permissions as an attribute (see, for example,
    +        // https://github.com/jenkinsci/jenkins/pull/3161 )
    +        final Path tempPath;
    +        final String tempDirNamePrefix = "jenkins";
    +        if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
    +            tempPath = Files.createTempDirectory(tempDirNamePrefix,
    +                    PosixFilePermissions.asFileAttribute(EnumSet.allOf(PosixFilePermission.class)));
    +        } else {
    +            tempPath = Files.createTempDirectory(tempDirNamePrefix);
    +        }
    +        return tempPath.toFile();
         }
     
         private static final Pattern errorCodeParser = Pattern.compile(".*CreateProcess.*error=([0-9]+).*");
    @@ -611,7 +437,7 @@ public class Util {
                     try {
                         ResourceBundle rb = ResourceBundle.getBundle("/hudson/win32errors");
                         return rb.getString("error"+m.group(1));
    -                } catch (Exception _) {
    +                } catch (Exception ignored) {
                         // silently recover from resource related failures
                     }
                 }
    @@ -656,10 +482,7 @@ public class Util {
          */
         @Deprecated
         public static void copyStream(@Nonnull InputStream in,@Nonnull OutputStream out) throws IOException {
    -        byte[] buf = new byte[8192];
    -        int len;
    -        while((len=in.read(buf))>=0)
    -            out.write(buf,0,len);
    +        IOUtils.copy(in, out);
         }
     
         /**
    @@ -667,10 +490,7 @@ public class Util {
          */
         @Deprecated
         public static void copyStream(@Nonnull Reader in, @Nonnull Writer out) throws IOException {
    -        char[] buf = new char[8192];
    -        int len;
    -        while((len=in.read(buf))>0)
    -            out.write(buf,0,len);
    +        IOUtils.copy(in, out);
         }
     
         /**
    @@ -679,7 +499,7 @@ public class Util {
         @Deprecated
         public static void copyStreamAndClose(@Nonnull InputStream in, @Nonnull OutputStream out) throws IOException {
             try (InputStream _in = in; OutputStream _out = out) { // make sure both are closed, and use Throwable.addSuppressed
    -            copyStream(in,out);
    +            IOUtils.copy(_in, _out);
             }
         }
     
    @@ -689,7 +509,7 @@ public class Util {
         @Deprecated
         public static void copyStreamAndClose(@Nonnull Reader in, @Nonnull Writer out) throws IOException {
             try (Reader _in = in; Writer _out = out) {
    -            copyStream(in,out);
    +            IOUtils.copy(_in, _out);
             }
         }
     
    @@ -779,15 +599,15 @@ public class Util {
         public static String getDigestOf(@Nonnull InputStream source) throws IOException {
             try {
                 MessageDigest md5 = MessageDigest.getInstance("MD5");
    -
    -            byte[] buffer = new byte[1024];
    -            try (DigestInputStream in = new DigestInputStream(source, md5)) {
    -                while (in.read(buffer) >= 0)
    -                    ; // simply discard the input
    -            }
    +            DigestInputStream in = new DigestInputStream(source, md5);
    +            // Note: IOUtils.copy() buffers the input internally, so there is no
    +            // need to use a BufferedInputStream.
    +            IOUtils.copy(in, NullOutputStream.NULL_OUTPUT_STREAM);
                 return toHexString(md5.digest());
             } catch (NoSuchAlgorithmException e) {
                 throw new IOException("MD5 not installed",e);    // impossible
    +        } finally {
    +            source.close();
             }
             /* JENKINS-18178: confuses Maven 2 runner
             try {
    @@ -801,7 +621,7 @@ public class Util {
         @Nonnull
         public static String getDigestOf(@Nonnull String text) {
             try {
    -            return getDigestOf(new ByteArrayInputStream(text.getBytes("UTF-8")));
    +            return getDigestOf(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
             } catch (IOException e) {
                 throw new Error(e);
             }
    @@ -816,11 +636,8 @@ public class Util {
          */
         @Nonnull
         public static String getDigestOf(@Nonnull File file) throws IOException {
    -        try (InputStream is = Files.newInputStream(file.toPath())) {
    -            return getDigestOf(new BufferedInputStream(is));
    -        } catch (InvalidPathException e) {
    -            throw new IOException(e);
    -        }
    +        // Note: getDigestOf() closes the input stream.
    +        return getDigestOf(Files.newInputStream(fileToPath(file)));
         }
     
         /**
    @@ -833,14 +650,12 @@ public class Util {
                 // turn secretKey into 256 bit hash
                 MessageDigest digest = MessageDigest.getInstance("SHA-256");
                 digest.reset();
    -            digest.update(s.getBytes("UTF-8"));
    +            digest.update(s.getBytes(StandardCharsets.UTF_8));
     
                 // Due to the stupid US export restriction JDK only ships 128bit version.
                 return new SecretKeySpec(digest.digest(),0,128/8, "AES");
             } catch (NoSuchAlgorithmException e) {
                 throw new Error(e);
    -        } catch (UnsupportedEncodingException e) {
    -            throw new Error(e);
             }
         }
     
    @@ -862,6 +677,8 @@ public class Util {
     
         @Nonnull
         public static byte[] fromHexString(@Nonnull String data) {
    +        if (data.length() % 2 != 0)
    +            throw new IllegalArgumentException("data must have an even number of hexadecimal digits");
             byte[] r = new byte[data.length() / 2];
             for (int i = 0; i < data.length(); i += 2)
                 r[i / 2] = (byte) Integer.parseInt(data.substring(i, i + 2), 16);
    @@ -992,7 +809,7 @@ public class Util {
                 StringBuilder out = new StringBuilder(s.length());
     
                 ByteArrayOutputStream buf = new ByteArrayOutputStream();
    -            OutputStreamWriter w = new OutputStreamWriter(buf,"UTF-8");
    +            OutputStreamWriter w = new OutputStreamWriter(buf, StandardCharsets.UTF_8);
     
                 for (int i = 0; i < s.length(); i++) {
                     int c = s.charAt(i);
    @@ -1055,7 +872,7 @@ public class Util {
                     if (!escaped) {
                         out = new StringBuilder(i + (m - i) * 3);
                         out.append(s.substring(0, i));
    -                    enc = Charset.forName("UTF-8").newEncoder();
    +                    enc = StandardCharsets.UTF_8.newEncoder();
                         buf = CharBuffer.allocate(1);
                         escaped = true;
                     }
    @@ -1092,8 +909,8 @@ public class Util {
         /**
          * Escapes HTML unsafe characters like &lt;, &amp; to the respective character entities.
          */
    -    @Nonnull
    -    public static String escape(@Nonnull String text) {
    +    @Nullable
    +    public static String escape(@CheckForNull String text) {
             if (text==null)     return null;
             StringBuilder buf = new StringBuilder(text.length()+64);
             for( int i=0; i<text.length(); i++ ) {
    @@ -1149,14 +966,13 @@ public class Util {
         }
     
         /**
    -     * Creates an empty file.
    +     * Creates an empty file if nonexistent or truncates the existing file.
    +     * Note: The behavior of this method in the case where the file already
    +     * exists is unlike the POSIX <code>touch</code> utility which merely
    +     * updates the file's access and/or modification time.
          */
         public static void touch(@Nonnull File file) throws IOException {
    -        try {
    -            Files.newOutputStream(file.toPath()).close();
    -        } catch (InvalidPathException e) {
    -            throw new IOException(e);
    -        }
    +        Files.newOutputStream(fileToPath(file)).close();
         }
     
         /**
    @@ -1176,8 +992,17 @@ public class Util {
          */
         @Nonnull
         public static String fixNull(@CheckForNull String s) {
    -        if(s==null)     return "";
    -        else            return s;
    +        return fixNull(s, "");
    +    }
    +
    +    /**
    +     * Convert {@code null} to a default value.
    +     * @param defaultValue Default value. It may be immutable or not, depending on the implementation.
    +     * @since 2.144
    +     */
    +    @Nonnull
    +    public static <T> T fixNull(@CheckForNull T s, @Nonnull T defaultValue) {
    +        return s != null ? s : defaultValue;
         }
     
         /**
    @@ -1200,24 +1025,60 @@ public class Util {
             return fixEmpty(s.trim());
         }
     
    +    /**
    +     *
    +     * @param l list to check.
    +     * @param <T>
    +     *     Type of the list.
    +     * @return
    +     *     {@code l} if l is not {@code null}.
    +     *     An empty <b>immutable list</b> if l is {@code null}.
    +     */
         @Nonnull
         public static <T> List<T> fixNull(@CheckForNull List<T> l) {
    -        return l!=null ? l : Collections.<T>emptyList();
    +        return fixNull(l, Collections.<T>emptyList());
         }
     
    +    /**
    +     *
    +     * @param l set to check.
    +     * @param <T>
    +     *     Type of the set.
    +     * @return
    +     *     {@code l} if l is not {@code null}.
    +     *     An empty <b>immutable set</b> if l is {@code null}.
    +     */
         @Nonnull
         public static <T> Set<T> fixNull(@CheckForNull Set<T> l) {
    -        return l!=null ? l : Collections.<T>emptySet();
    +        return fixNull(l, Collections.<T>emptySet());
         }
     
    +    /**
    +     *
    +     * @param l collection to check.
    +     * @param <T>
    +     *     Type of the collection.
    +     * @return
    +     *     {@code l} if l is not {@code null}.
    +     *     An empty <b>immutable set</b> if l is {@code null}.
    +     */
         @Nonnull
         public static <T> Collection<T> fixNull(@CheckForNull Collection<T> l) {
    -        return l!=null ? l : Collections.<T>emptySet();
    +        return fixNull(l, Collections.<T>emptySet());
         }
     
    +    /**
    +     *
    +     * @param l iterable to check.
    +     * @param <T>
    +     *     Type of the iterable.
    +     * @return
    +     *     {@code l} if l is not {@code null}.
    +     *     An empty <b>immutable set</b> if l is {@code null}.
    +     */
         @Nonnull
         public static <T> Iterable<T> fixNull(@CheckForNull Iterable<T> l) {
    -        return l!=null ? l : Collections.<T>emptySet();
    +        return fixNull(l, Collections.<T>emptySet());
         }
     
         /**
    @@ -1323,80 +1184,8 @@ public class Util {
         public static void createSymlink(@Nonnull File baseDir, @Nonnull String targetPath,
                 @Nonnull String symlinkPath, @Nonnull TaskListener listener) throws InterruptedException {
             try {
    -            if (createSymlinkJava7(baseDir, targetPath, symlinkPath)) {
    -                return;
    -            }
    -            if (NO_SYMLINK) {
    -                return;
    -            }
    -
    -            File symlinkFile = new File(baseDir, symlinkPath);
    -            if (Functions.isWindows()) {
    -                if (symlinkFile.exists()) {
    -                    symlinkFile.delete();
    -                }
    -                File dst = new File(symlinkFile,"..\\"+targetPath);
    -                try {
    -                    Kernel32Utils.createSymbolicLink(symlinkFile,targetPath,dst.isDirectory());
    -                } catch (WinIOException e) {
    -                    if (e.getErrorCode()==1314) {/* ERROR_PRIVILEGE_NOT_HELD */
    -                        warnWindowsSymlink();
    -                        return;
    -                    }
    -                    throw e;
    -                } catch (UnsatisfiedLinkError e) {
    -                    // not available on this Windows
    -                    return;
    -                }
    -            } else {
    -                String errmsg = "";
    -                // if a file or a directory exists here, delete it first.
    -                // try simple delete first (whether exists() or not, as it may be symlink pointing
    -                // to non-existent target), but fallback to "rm -rf" to delete non-empty dir.
    -                if (!symlinkFile.delete() && symlinkFile.exists())
    -                    // ignore a failure.
    -                    new LocalProc(new String[]{"rm","-rf", symlinkPath},new String[0],listener.getLogger(), baseDir).join();
    -
    -                Integer r=null;
    -                if (!SYMLINK_ESCAPEHATCH) {
    -                    try {
    -                        r = LIBC.symlink(targetPath,symlinkFile.getAbsolutePath());
    -                        if (r!=0) {
    -                            r = Native.getLastError();
    -                            errmsg = LIBC.strerror(r);
    -                        }
    -                    } catch (LinkageError e) {
    -                        // if JNA is unavailable, fall back.
    -                        // we still prefer to try JNA first as PosixAPI supports even smaller platforms.
    -                        POSIX posix = PosixAPI.jnr();
    -                        if (posix.isNative()) {
    -                            // TODO should we rethrow PosixException as IOException here?
    -                            r = posix.symlink(targetPath,symlinkFile.getAbsolutePath());
    -                        }
    -                    }
    -                }
    -                if (r==null) {
    -                    // if all else fail, fall back to the most expensive approach of forking a process
    -                    // TODO is this really necessary? JavaPOSIX should do this automatically
    -                    r = new LocalProc(new String[]{
    -                        "ln","-s", targetPath, symlinkPath},
    -                        new String[0],listener.getLogger(), baseDir).join();
    -                }
    -                if (r!=0)
    -                    listener.getLogger().println(String.format("ln -s %s %s failed: %d %s",targetPath, symlinkFile, r, errmsg));
    -            }
    -        } catch (IOException e) {
    -            PrintStream log = listener.getLogger();
    -            log.printf("ln %s %s failed%n",targetPath, new File(baseDir, symlinkPath));
    -            Util.displayIOException(e,listener);
    -            Functions.printStackTrace(e, log);
    -        }
    -    }
    -
    -    private static boolean createSymlinkJava7(@Nonnull File baseDir, @Nonnull String targetPath, @Nonnull String symlinkPath) throws IOException {
    -        try {
    -            Path path = new File(baseDir, symlinkPath).toPath();
    -            Path target = Paths.get(targetPath, new String[0]);
    +            Path path = fileToPath(new File(baseDir, symlinkPath));
    +            Path target = Paths.get(targetPath, MemoryReductionUtil.EMPTY_STRING_ARRAY);
     
                 final int maxNumberOfTries = 4;
                 final int timeInMillis = 100;
    @@ -1410,23 +1199,22 @@ public class Util {
                             TimeUnit.MILLISECONDS.sleep(timeInMillis); //trying to defeat likely ongoing race condition
                             continue;
                         }
    -                    LOGGER.warning("symlink FileAlreadyExistsException thrown " + maxNumberOfTries + " times => cannot createSymbolicLink");
    +                    LOGGER.log(Level.WARNING, "symlink FileAlreadyExistsException thrown {0} times => cannot createSymbolicLink", maxNumberOfTries);
                         throw fileAlreadyExistsException;
                     }
                 }
    -            return true;
             } catch (UnsupportedOperationException e) {
    -                return true; // no symlinks on this platform
    -        } catch (FileSystemException e) {
    -            if (Functions.isWindows()) {
    +            PrintStream log = listener.getLogger();
    +            log.print("Symbolic links are not supported on this platform");
    +            Functions.printStackTrace(e, log);
    +        } catch (IOException e) {
    +            if (Functions.isWindows() && e instanceof FileSystemException) {
                     warnWindowsSymlink();
    -                return true;
    +                return;
                 }
    -            return false;
    -        } catch (IOException x) {
    -            throw x;
    -        } catch (Exception x) {
    -            throw (IOException) new IOException(x.toString()).initCause(x);
    +            PrintStream log = listener.getLogger();
    +            log.printf("ln %s %s failed%n",targetPath, new File(baseDir, symlinkPath));
    +            Functions.printStackTrace(e, log);
             }
         }
     
    @@ -1474,9 +1262,9 @@ public class Util {
          *      The relative path is meant to be resolved from the location of the symlink.
          */
         @CheckForNull
    -    public static String resolveSymlink(@Nonnull File link) throws InterruptedException, IOException {
    +    public static String resolveSymlink(@Nonnull File link) throws IOException {
             try {
    -            Path path =  link.toPath();
    +            Path path = fileToPath(link);
                 return Files.readSymbolicLink(path).toString();
             } catch (UnsupportedOperationException | FileSystemException x) {
                 // no symlinks on this platform (windows?),
    @@ -1486,7 +1274,7 @@ public class Util {
             } catch (IOException x) {
                 throw x;
             } catch (Exception x) {
    -            throw (IOException) new IOException(x.toString()).initCause(x);
    +            throw new IOException(x);
             }
         }
     
    @@ -1507,7 +1295,7 @@ public class Util {
             try {
                 return new URI(null,url,null).toASCIIString();
             } catch (URISyntaxException e) {
    -            LOGGER.warning("Failed to encode "+url);    // could this ever happen?
    +            LOGGER.log(Level.WARNING, "Failed to encode {0}", url);    // could this ever happen?
                 return url;
             }
         }
    @@ -1665,9 +1453,91 @@ public class Util {
             try {
                 toClose.close();
             } catch(IOException ex) {
    -            logger.log(Level.WARNING, String.format("Failed to close %s of %s", closeableName, closeableOwner), ex);
    +            LogRecord record = new LogRecord(Level.WARNING, "Failed to close {0} of {1}");
    +            record.setParameters(new Object[] { closeableName, closeableOwner });
    +            record.setThrown(ex);
    +            logger.log(record);
    +        }
    +    }
    +
    +    @Restricted(NoExternalUse.class)
    +    public static int permissionsToMode(Set<PosixFilePermission> permissions) {
    +        PosixFilePermission[] allPermissions = PosixFilePermission.values();
    +        int result = 0;
    +        for (int i = 0; i < allPermissions.length; i++) {
    +            result <<= 1;
    +            result |= permissions.contains(allPermissions[i]) ? 1 : 0;
    +        }
    +        return result;
    +    }
    +
    +    @Restricted(NoExternalUse.class)
    +    public static Set<PosixFilePermission> modeToPermissions(int mode) throws IOException {
    +         // Anything larger is a file type, not a permission.
    +        int PERMISSIONS_MASK = 07777;
    +        // setgid/setuid/sticky are not supported.
    +        int MAX_SUPPORTED_MODE = 0777;
    +        mode = mode & PERMISSIONS_MASK;
    +        if ((mode & MAX_SUPPORTED_MODE) != mode) {
    +            throw new IOException("Invalid mode: " + mode);
    +        }
    +        PosixFilePermission[] allPermissions = PosixFilePermission.values();
    +        Set<PosixFilePermission> result = EnumSet.noneOf(PosixFilePermission.class);
    +        for (int i = 0; i < allPermissions.length; i++) {
    +            if ((mode & 1) == 1) {
    +                result.add(allPermissions[allPermissions.length - i - 1]);
    +            }
    +            mode >>= 1;
    +        }
    +        return result;
    +    }
    +
    +    /**
    +     * Converts a {@link File} into a {@link Path} and checks runtime exceptions.
    +     * @throws IOException if {@code f.toPath()} throws {@link InvalidPathException}.
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static @Nonnull Path fileToPath(@Nonnull File file) throws IOException {
    +        try {
    +            return file.toPath();
    +        } catch (InvalidPathException e) {
    +            throw new IOException(e);
             }
         }
    +    
    +    /**
    +     * Compute the number of calendar days elapsed since the given date.
    +     * As it's only the calendar days difference that matter, "11.00pm" to "2.00am the day after" returns 1,
    +     * even if there are only 3 hours between. As well as "10am" to "2pm" both on the same day, returns 0.
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static long daysBetween(@Nonnull Date a, @Nonnull Date b){
    +        LocalDate aLocal = a.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    +        LocalDate bLocal = b.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    +        return ChronoUnit.DAYS.between(aLocal, bLocal);
    +    }
    +    
    +    /**
    +     * @return positive number of days between the given date and now
    +     * @see #daysBetween(Date, Date)
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static long daysElapsedSince(@Nonnull Date date){
    +        return Math.max(0, daysBetween(date, new Date()));
    +    }
    +    
    +    /**
    +     * Find the specific ancestor, or throw an exception.
    +     * Useful for an ancestor we know is inside the URL to ease readability
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static @Nonnull <T> T getNearestAncestorOfTypeOrThrow(@Nonnull StaplerRequest request, @Nonnull Class<T> clazz) {
    +        T t = request.findAncestorObject(clazz);
    +        if (t == null) {
    +            throw new IllegalArgumentException("No ancestor of type " + clazz.getName() + " in the request");
    +        }
    +        return t;
    +    }
     
         public static final FastDateFormat XS_DATETIME_FORMATTER = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'",new SimpleTimeZone(0,"GMT"));
     
    @@ -1727,7 +1597,7 @@ public class Util {
          * Warning: This should only ever be used if you find that your builds are
          * failing because Jenkins is unable to delete files, that this failure is
          * because Jenkins itself has those files locked "open", and even then it
    -     * should only be used on slaves with relatively few executors (because the
    +     * should only be used on agents with relatively few executors (because the
          * garbage collection can impact the performance of all job executors on
          * that slave).<br/>
          * i.e. Setting this flag is a act of last resort - it is <em>not</em>
    @@ -1736,4 +1606,19 @@ public class Util {
          */
         @Restricted(value = NoExternalUse.class)
         static boolean GC_AFTER_FAILED_DELETE = SystemProperties.getBoolean(Util.class.getName() + ".performGCOnFailedDelete");
    +
    +    private static PathRemover newPathRemover(@Nonnull PathRemover.PathChecker pathChecker) {
    +        return PathRemover.newFilteredRobustRemover(pathChecker, DELETION_MAX - 1, GC_AFTER_FAILED_DELETE, WAIT_BETWEEN_DELETION_RETRIES);
    +    }
    +
    +    /**
    +     * If this flag is true, native implementations of {@link FilePath#chmod}
    +     * and {@link hudson.util.IOUtils#mode} are used instead of NIO.
    +     * <p>
    +     * This should only be enabled if the setgid/setuid/sticky bits are
    +     * intentionally set on the Jenkins installation and they are being
    +     * overwritten by Jenkins erroneously.
    +     */
    +    @Restricted(value = NoExternalUse.class)
    +    public static boolean NATIVE_CHMOD_MODE = SystemProperties.getBoolean(Util.class.getName() + ".useNativeChmodAndMode");
     }
    diff --git a/core/src/main/java/hudson/WebAppMain.java b/core/src/main/java/hudson/WebAppMain.java
    index d9b11ccfb53877ee3df9488d221ef8a1c20417d4..927b533424928598582fa3de1e33ae12e27f95ea 100644
    --- a/core/src/main/java/hudson/WebAppMain.java
    +++ b/core/src/main/java/hudson/WebAppMain.java
    @@ -31,7 +31,6 @@ import java.nio.file.StandardOpenOption;
     import jenkins.util.SystemProperties;
     import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
     import com.thoughtworks.xstream.core.JVM;
    -import com.trilead.ssh2.util.IOUtils;
     import hudson.model.Hudson;
     import hudson.security.ACL;
     import hudson.util.BootFailure;
    @@ -62,7 +61,6 @@ import javax.servlet.ServletResponse;
     import javax.xml.transform.TransformerFactory;
     import javax.xml.transform.TransformerFactoryConfigurationError;
     import java.io.File;
    -import java.io.FileOutputStream;
     import java.io.IOException;
     import java.net.URL;
     import java.net.URLClassLoader;
    diff --git a/core/src/main/java/hudson/XmlFile.java b/core/src/main/java/hudson/XmlFile.java
    index 72d4d6c128ac605d4abd74d0d0587fb2d39af508..7f630dfb33dc23bf1021ae16f0a0cfd081a82f7a 100644
    --- a/core/src/main/java/hudson/XmlFile.java
    +++ b/core/src/main/java/hudson/XmlFile.java
    @@ -24,11 +24,9 @@
     package hudson;
     
     import com.thoughtworks.xstream.XStream;
    -import com.thoughtworks.xstream.XStreamException;
     import com.thoughtworks.xstream.converters.Converter;
     import com.thoughtworks.xstream.converters.UnmarshallingContext;
    -import com.thoughtworks.xstream.io.StreamException;
    -import com.thoughtworks.xstream.io.xml.Xpp3Driver;
    +import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
     import hudson.diagnosis.OldDataMonitor;
     import hudson.model.Descriptor;
     import hudson.util.AtomicFileWriter;
    @@ -41,7 +39,6 @@ import org.xml.sax.Locator;
     import org.xml.sax.SAXException;
     import org.xml.sax.ext.Locator2;
     import org.xml.sax.helpers.DefaultHandler;
    -
     import javax.xml.parsers.ParserConfigurationException;
     import javax.xml.parsers.SAXParserFactory;
     import java.io.BufferedInputStream;
    @@ -50,8 +47,13 @@ import java.io.IOException;
     import java.io.InputStream;
     import java.io.InputStreamReader;
     import java.io.Reader;
    +import java.io.Serializable;
     import java.io.Writer;
     import java.io.StringWriter;
    +import java.util.Collections;
    +import java.util.IdentityHashMap;
    +import java.util.Map;
    +import java.util.function.Supplier;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import org.apache.commons.io.IOUtils;
    @@ -70,12 +72,12 @@ import org.apache.commons.io.IOUtils;
      * not have any data, the newly added field is left to the VM-default
      * value (if you let XStream create the object, such as
      * {@link #read()} &mdash; which is the majority), or to the value initialized by the
    - * constructor (if the object is created via <tt>new</tt> and then its
    + * constructor (if the object is created via {@code new} and then its
      * value filled by XStream, such as {@link #unmarshal(Object)}.)
      *
      * <p>
      * Removing a field requires that you actually leave the field with
    - * <tt>transient</tt> keyword. When you read the old XML, XStream
    + * {@code transient} keyword. When you read the old XML, XStream
      * will set the value to this field. But when the data is saved,
      * the field will no longer will be written back to XML.
      * (It might be possible to tweak XStream so that we can simply
    @@ -83,13 +85,13 @@ import org.apache.commons.io.IOUtils;
      *
      * <p>
      * Changing the data structure is usually a combination of the two
    - * above. You'd leave the old data store with <tt>transient</tt>,
    + * above. You'd leave the old data store with {@code transient},
      * and then add the new data. When you are reading the old XML,
      * only the old field will be set. When you are reading the new XML,
      * only the new field will be set. You'll then need to alter the code
      * so that it will be able to correctly handle both situations,
      * and that as soon as you see data in the old field, you'll have to convert
    - * that into the new data structure, so that the next <tt>save</tt> operation
    + * that into the new data structure, so that the next {@code save} operation
      * will write the new data (otherwise you'll end up losing the data, because
      * old fields will be never written back.)
      *
    @@ -114,6 +116,8 @@ import org.apache.commons.io.IOUtils;
     public final class XmlFile {
         private final XStream xs;
         private final File file;
    +    private static final Map<Object, Void> beingWritten = Collections.synchronizedMap(new IdentityHashMap<>());
    +    private static final ThreadLocal<File> writing = new ThreadLocal<>();
     
         public XmlFile(File file) {
             this(DEFAULT_XSTREAM,file);
    @@ -141,7 +145,7 @@ public final class XmlFile {
             }
             try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
                 return xs.fromXML(in);
    -        } catch (XStreamException | Error | InvalidPathException e) {
    +        } catch (RuntimeException | Error e) {
                 throw new IOException("Unable to read "+file,e);
             }
         }
    @@ -150,15 +154,30 @@ public final class XmlFile {
          * Loads the contents of this file into an existing object.
          *
          * @return
    -     *      The unmarshalled object. Usually the same as <tt>o</tt>, but would be different
    +     *      The unmarshalled object. Usually the same as {@code o}, but would be different
          *      if the XML representation is completely new.
          */
         public Object unmarshal( Object o ) throws IOException {
    +        return unmarshal(o, false);
    +    }
    +
    +    /**
    +     * Variant of {@link #unmarshal(Object)} applying {@link XStream2#unmarshal(HierarchicalStreamReader, Object, DataHolder, boolean)}.
    +     * @since 2.99
    +     */
    +    public Object unmarshalNullingOut(Object o) throws IOException {
    +        return unmarshal(o, true);
    +    }
     
    +    private Object unmarshal(Object o, boolean nullOut) throws IOException {
             try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
                 // TODO: expose XStream the driver from XStream
    -            return xs.unmarshal(DEFAULT_DRIVER.createReader(in), o);
    -        } catch (XStreamException | Error | InvalidPathException e) {
    +            if (nullOut) {
    +                return ((XStream2) xs).unmarshal(DEFAULT_DRIVER.createReader(in), o, null, true);
    +            } else {
    +                return xs.unmarshal(DEFAULT_DRIVER.createReader(in), o);
    +            }
    +        } catch (RuntimeException | Error e) {
                 throw new IOException("Unable to read "+file,e);
             }
         }
    @@ -167,16 +186,44 @@ public final class XmlFile {
             mkdirs();
             AtomicFileWriter w = new AtomicFileWriter(file);
             try {
    -            w.write("<?xml version='1.0' encoding='UTF-8'?>\n");
    -            xs.toXML(o,w);
    +            w.write("<?xml version='1.1' encoding='UTF-8'?>\n");
    +            beingWritten.put(o, null);
    +            writing.set(file);
    +            try {
    +                xs.toXML(o, w);
    +            } finally {
    +                beingWritten.remove(o);
    +                writing.set(null);
    +            }
                 w.commit();
    -        } catch(StreamException e) {
    +        } catch(RuntimeException e) {
                 throw new IOException(e);
             } finally {
                 w.abort();
             }
         }
     
    +    /**
    +     * Provides an XStream replacement for an object unless a call to {@link #write} is currently in progress.
    +     * As per JENKINS-45892 this may be used by any class which expects to be written at top level to an XML file
    +     * but which cannot safely be serialized as a nested object (for example, because it expects some {@code onLoad} hook):
    +     * implement a {@code writeReplace} method delegating to this method.
    +     * The replacement need not be {@link Serializable} since it is only necessary for use from XStream.
    +     * @param o an object ({@code this} from {@code writeReplace})
    +     * @param replacement a supplier of a safely serializable replacement object with a {@code readResolve} method
    +     * @return {@code o}, if {@link #write} is being called on it, else the replacement
    +     * @since 2.74
    +     */
    +    public static Object replaceIfNotAtTopLevel(Object o, Supplier<Object> replacement) {
    +        File currentlyWriting = writing.get();
    +        if (beingWritten.containsKey(o) || currentlyWriting == null) {
    +            return o;
    +        } else {
    +            LOGGER.log(Level.WARNING, "JENKINS-45892: reference to " + o + " being saved from unexpected " + currentlyWriting, new IllegalStateException());
    +            return replacement.get();
    +        }
    +    }
    +
         public boolean exists() {
             return file.exists();
         }
    @@ -304,13 +351,14 @@ public final class XmlFile {
         /**
          * {@link XStream} instance is supposed to be thread-safe.
          */
    -    private static final XStream DEFAULT_XSTREAM = new XStream2();
     
         private static final Logger LOGGER = Logger.getLogger(XmlFile.class.getName());
     
         private static final SAXParserFactory JAXP = SAXParserFactory.newInstance();
     
    -    private static final Xpp3Driver DEFAULT_DRIVER = new Xpp3Driver();
    +    private static final HierarchicalStreamDriver DEFAULT_DRIVER = XStream2.getDefaultDriver();
    +
    +    private static final XStream DEFAULT_XSTREAM = new XStream2(DEFAULT_DRIVER);
     
         static {
             JAXP.setNamespaceAware(true);
    diff --git a/core/src/main/java/hudson/cli/BuildCommand.java b/core/src/main/java/hudson/cli/BuildCommand.java
    index cd6a13efa1dbae62a404f3e50a73a332b02394a2..8cd09281c52c1eb930db6dede5900c1731d42f81 100644
    --- a/core/src/main/java/hudson/cli/BuildCommand.java
    +++ b/core/src/main/java/hudson/cli/BuildCommand.java
    @@ -89,7 +89,7 @@ public class BuildCommand extends CLICommand {
         public boolean checkSCM = false;
     
         @Option(name="-p",usage="Specify the build parameters in the key=value format.")
    -    public Map<String,String> parameters = new HashMap<String, String>();
    +    public Map<String,String> parameters = new HashMap<>();
     
         @Option(name="-v",usage="Prints out the console output of the build. Use with -s")
         public boolean consoleOutput = false;
    @@ -109,7 +109,7 @@ public class BuildCommand extends CLICommand {
                     throw new IllegalStateException(job.getFullDisplayName()+" is not parameterized but the -p option was specified.");
     
                 //TODO: switch to type annotations after the migration to Java 1.8
    -            List<ParameterValue> values = new ArrayList<ParameterValue>();
    +            List<ParameterValue> values = new ArrayList<>();
     
                 for (Entry<String, String> e : parameters.entrySet()) {
                     String name = e.getKey();
    diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java
    index 2c37db46cc0ed3e52184fe7d9ef9cb264bbca21b..f9b0ef6d92472d9bf677e9003049805d7bf9d66b 100644
    --- a/core/src/main/java/hudson/cli/CLIAction.java
    +++ b/core/src/main/java/hudson/cli/CLIAction.java
    @@ -250,7 +250,7 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
                 // do not require any permission to establish a CLI connection
                 // the actual authentication for the connecting Channel is done by CLICommand
     
    -            return new FullDuplexHttpChannel(uuid, !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
    +            return new FullDuplexHttpChannel(uuid, !Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
                     @SuppressWarnings("deprecation")
                     @Override
                     protected void main(Channel channel) throws IOException, InterruptedException {
    diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java
    index d507662037b575cc82fab21ed0fcb21c9e764850..879f8989af41de507ba42d35614a29f8b63c7270 100644
    --- a/core/src/main/java/hudson/cli/CLICommand.java
    +++ b/core/src/main/java/hudson/cli/CLICommand.java
    @@ -30,6 +30,8 @@ import hudson.ExtensionPoint;
     import hudson.cli.declarative.CLIMethod;
     import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
     import hudson.Functions;
    +import hudson.security.ACL;
    +import jenkins.security.SecurityListener;
     import jenkins.util.SystemProperties;
     import hudson.cli.declarative.OptionHandlerExtension;
     import jenkins.model.Jenkins;
    @@ -44,6 +46,8 @@ import org.acegisecurity.Authentication;
     import org.acegisecurity.BadCredentialsException;
     import org.acegisecurity.context.SecurityContext;
     import org.acegisecurity.context.SecurityContextHolder;
    +import org.acegisecurity.userdetails.User;
    +import org.acegisecurity.userdetails.UserDetails;
     import org.apache.commons.discovery.ResourceClassIterator;
     import org.apache.commons.discovery.ResourceNameIterator;
     import org.apache.commons.discovery.resource.ClassLoaders;
    @@ -80,7 +84,7 @@ import javax.annotation.Nonnull;
      * <h2>How does a CLI command work</h2>
      * <p>
      * The users starts {@linkplain CLI the "CLI agent"} on a remote system, by specifying arguments, like
    - * <tt>"java -jar jenkins-cli.jar command arg1 arg2 arg3"</tt>. The CLI agent creates
    + * {@code "java -jar jenkins-cli.jar command arg1 arg2 arg3"}. The CLI agent creates
      * a remoting channel with the server, and it sends the entire arguments to the server, along with
      * the remoted stdin/out/err.
      *
    @@ -179,7 +183,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
          * Gets the command name.
          *
          * <p>
    -     * For example, if the CLI is invoked as <tt>java -jar cli.jar foo arg1 arg2 arg4</tt>,
    +     * For example, if the CLI is invoked as {@code java -jar cli.jar foo arg1 arg2 arg4},
          * on the server side {@link CLICommand} that returns "foo" from {@link #getName()}
          * will be invoked.
          *
    @@ -256,6 +260,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
             // add options from the authenticator
             SecurityContext sc = null;
             Authentication old = null;
    +        Authentication auth = null;
             try {
                 sc = SecurityContextHolder.getContext();
                 old = sc.getAuthentication();
    @@ -264,33 +269,50 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
                 sc.setAuthentication(getTransportAuthentication());
                 new ClassParser().parse(authenticator,p);
     
    +            if (!(this instanceof LoginCommand || this instanceof LogoutCommand || this instanceof HelpCommand || this instanceof WhoAmICommand))
    +                Jenkins.getActiveInstance().checkPermission(Jenkins.READ);
                 p.parseArgument(args.toArray(new String[args.size()]));
    -            Authentication auth = authenticator.authenticate();
    +            auth = authenticator.authenticate();
                 if (auth==Jenkins.ANONYMOUS)
                     auth = loadStoredAuthentication();
                 sc.setAuthentication(auth); // run the CLI with the right credential
    -            if (!(this instanceof LoginCommand || this instanceof HelpCommand))
    +            if (!(this instanceof LoginCommand || this instanceof LogoutCommand || this instanceof HelpCommand || this instanceof WhoAmICommand))
                     Jenkins.getActiveInstance().checkPermission(Jenkins.READ);
    -            return run();
    +            LOGGER.log(Level.FINE, "Invoking CLI command {0}, with {1} arguments, as user {2}.",
    +                    new Object[] {getName(), args.size(), auth.getName()});
    +            int res = run();
    +            LOGGER.log(Level.FINE, "Executed CLI command {0}, with {1} arguments, as user {2}, return code {3}",
    +                    new Object[] {getName(), args.size(), auth.getName(), res});
    +            return res;
             } catch (CmdLineException e) {
    +            LOGGER.log(Level.FINE, String.format("Failed call to CLI command %s, with %d arguments, as user %s.",
    +                    getName(), args.size(), auth != null ? auth.getName() : "<unknown>"), e);
                 stderr.println("");
                 stderr.println("ERROR: " + e.getMessage());
                 printUsage(stderr, p);
                 return 2;
             } catch (IllegalStateException e) {
    +            LOGGER.log(Level.FINE, String.format("Failed call to CLI command %s, with %d arguments, as user %s.",
    +                    getName(), args.size(), auth != null ? auth.getName() : "<unknown>"), e);
                 stderr.println("");
                 stderr.println("ERROR: " + e.getMessage());
                 return 4;
             } catch (IllegalArgumentException e) {
    +            LOGGER.log(Level.FINE, String.format("Failed call to CLI command %s, with %d arguments, as user %s.",
    +                    getName(), args.size(), auth != null ? auth.getName() : "<unknown>"), e);
                 stderr.println("");
                 stderr.println("ERROR: " + e.getMessage());
                 return 3;
             } catch (AbortException e) {
    +            LOGGER.log(Level.FINE, String.format("Failed call to CLI command %s, with %d arguments, as user %s.",
    +                    getName(), args.size(), auth != null ? auth.getName() : "<unknown>"), e);
                 // signals an error without stack trace
                 stderr.println("");
                 stderr.println("ERROR: " + e.getMessage());
                 return 5;
             } catch (AccessDeniedException e) {
    +            LOGGER.log(Level.FINE, String.format("Failed call to CLI command %s, with %d arguments, as user %s.",
    +                    getName(), args.size(), auth != null ? auth.getName() : "<unknown>"), e);
                 stderr.println("");
                 stderr.println("ERROR: " + e.getMessage());
                 return 6;
    @@ -344,8 +366,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
         @Deprecated
         protected Authentication loadStoredAuthentication() throws InterruptedException {
             try {
    -            if (channel!=null)
    -                return new ClientAuthenticationCache(channel).get();
    +            if (channel!=null){
    +                Authentication authLoadedFromCache = new ClientAuthenticationCache(channel).get();
    +
    +                if(!ACL.isAnonymous(authLoadedFromCache)){
    +                    UserDetails userDetails = new CLIUserDetails(authLoadedFromCache);
    +                    SecurityListener.fireAuthenticated(userDetails);
    +                }
    +
    +                return authLoadedFromCache;
    +            }
             } catch (IOException e) {
                 stderr.println("Failed to access the stored credential");
                 Functions.printStackTrace(e, stderr);  // recover
    @@ -606,9 +636,9 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
          * Key for {@link Channel#getProperty(Object)} that links to the {@link Authentication} object
          * which captures the identity of the client given by the transport layer.
          */
    -    public static final ChannelProperty<Authentication> TRANSPORT_AUTHENTICATION = new ChannelProperty<Authentication>(Authentication.class,"transportAuthentication");
    +    public static final ChannelProperty<Authentication> TRANSPORT_AUTHENTICATION = new ChannelProperty<>(Authentication.class, "transportAuthentication");
     
    -    private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<CLICommand>();
    +    private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<>();
     
         /*package*/ static CLICommand setCurrent(CLICommand cmd) {
             CLICommand old = getCurrent();
    @@ -626,7 +656,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
         static {
             // register option handlers that are defined
             ClassLoaders cls = new ClassLoaders();
    -        Jenkins j = Jenkins.getActiveInstance();
    +        Jenkins j = Jenkins.getInstanceOrNull();
             if (j!=null) {// only when running on the master
                 cls.put(j.getPluginManager().uberClassLoader);
     
    @@ -642,4 +672,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
                 }
             }
         }
    +
    +    /**
    +     * User details loaded from the CLI {@link ClientAuthenticationCache}
    +     * The user is never anonymous since it must be authenticated to be stored in the cache
    +     */
    +    @Deprecated
    +    @Restricted(NoExternalUse.class)
    +    private static class CLIUserDetails extends User {
    +        private CLIUserDetails(Authentication auth) {
    +            super(auth.getName(), "", true, true, true, true, auth.getAuthorities());
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/cli/ClearQueueCommand.java b/core/src/main/java/hudson/cli/ClearQueueCommand.java
    index 8e76a477aa12af83cf315b4c35a0e7253401a223..52cd7c4063880db362f7b6ea7df9580389fec70b 100644
    --- a/core/src/main/java/hudson/cli/ClearQueueCommand.java
    +++ b/core/src/main/java/hudson/cli/ClearQueueCommand.java
    @@ -26,7 +26,6 @@ package hudson.cli;
     
     import hudson.Extension;
     import jenkins.model.Jenkins;
    -import org.acegisecurity.AccessDeniedException;
     
     import java.util.logging.Logger;
     
    diff --git a/core/src/main/java/hudson/cli/CliManagerImpl.java b/core/src/main/java/hudson/cli/CliManagerImpl.java
    index 750a38d210af7cb8f267086346715b3981129b50..d799e6bf997a8a4223bcc4b9ea8c1ce26eb6c9ce 100644
    --- a/core/src/main/java/hudson/cli/CliManagerImpl.java
    +++ b/core/src/main/java/hudson/cli/CliManagerImpl.java
    @@ -90,7 +90,7 @@ public class CliManagerImpl implements CliEntryPoint, Serializable {
                 cmd.channel = Channel.current();
                 final CLICommand old = CLICommand.setCurrent(cmd);
                 try {
    -                transportAuth = Channel.current().getProperty(CLICommand.TRANSPORT_AUTHENTICATION);
    +                transportAuth = Channel.currentOrFail().getProperty(CLICommand.TRANSPORT_AUTHENTICATION);
                     cmd.setTransportAuth(transportAuth);
                     return cmd.main(args.subList(1,args.size()),locale, stdin, out, err);
                 } finally {
    @@ -99,7 +99,7 @@ public class CliManagerImpl implements CliEntryPoint, Serializable {
             }
     
             err.println("No such command: "+subCmd);
    -        new HelpCommand().main(Collections.<String>emptyList(), locale, stdin, out, err);
    +        new HelpCommand().main(Collections.emptyList(), locale, stdin, out, err);
             return -1;
         }
     
    diff --git a/core/src/main/java/hudson/cli/CliProtocol.java b/core/src/main/java/hudson/cli/CliProtocol.java
    index cdf25b033ff048d8bb6b5696253a3ef4afcc4830..30e18b9cb0efd5b92d24b294e1f843c6009994f8 100644
    --- a/core/src/main/java/hudson/cli/CliProtocol.java
    +++ b/core/src/main/java/hudson/cli/CliProtocol.java
    @@ -39,7 +39,7 @@ public class CliProtocol extends AgentProtocol {
          */
         @Override
         public boolean isOptIn() {
    -        return OPT_IN;
    +        return true;
         }
     
         @Override
    @@ -47,12 +47,17 @@ public class CliProtocol extends AgentProtocol {
             return jenkins.CLI.get().isEnabled() ? "CLI-connect" : null;
         }
     
    +    @Override
    +    public boolean isDeprecated() {
    +        return true;
    +    }
    +
         /**
          * {@inheritDoc}
          */
         @Override
         public String getDisplayName() {
    -        return "Jenkins CLI Protocol/1";
    +        return "Jenkins CLI Protocol/1 (deprecated, unencrypted)";
         }
     
         @Override
    @@ -104,14 +109,4 @@ public class CliProtocol extends AgentProtocol {
                 channel.join();
             }
         }
    -
    -    /**
    -     * A/B test turning off this protocol by default.
    -     */
    -    private static final boolean OPT_IN;
    -
    -    static {
    -        byte hash = Util.fromHexString(Jenkins.getInstance().getLegacyInstanceId())[0];
    -        OPT_IN = (hash % 10) == 0;
    -    }
     }
    diff --git a/core/src/main/java/hudson/cli/CliProtocol2.java b/core/src/main/java/hudson/cli/CliProtocol2.java
    index e7d0bad71fa4ccc152bc25a7a2eaaefe3d958101..6e181714bb93b88640b06d728ce6cefa1ac2ab38 100644
    --- a/core/src/main/java/hudson/cli/CliProtocol2.java
    +++ b/core/src/main/java/hudson/cli/CliProtocol2.java
    @@ -35,15 +35,21 @@ public class CliProtocol2 extends CliProtocol {
          */
         @Override
         public boolean isOptIn() {
    -        return false;
    +        return true;
         }
     
    +    @Override
    +    public boolean isDeprecated() {
    +        // We do not recommend it though it may be required for Remoting CLI
    +        return true;
    +    }
    +    
         /**
          * {@inheritDoc}
          */
         @Override
         public String getDisplayName() {
    -        return "Jenkins CLI Protocol/2";
    +        return "Jenkins CLI Protocol/2 (deprecated)";
         }
     
         @Override
    diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
    index 05e80bc515749b62274a63288091538991acefa8..611f3918d9d1b4ebd6378a8e8e96eddc65eeca9f 100644
    --- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
    +++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
    @@ -2,13 +2,16 @@ package hudson.cli;
     
     import com.google.common.annotations.VisibleForTesting;
     import hudson.FilePath;
    +import hudson.model.User;
     import hudson.remoting.Channel;
     import hudson.util.Secret;
     import jenkins.model.Jenkins;
     import jenkins.security.MasterToSlaveCallable;
    +import jenkins.security.seed.UserSeedProperty;
     import org.acegisecurity.Authentication;
     import org.acegisecurity.AuthenticationException;
     import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
    +import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
     import org.acegisecurity.userdetails.UserDetails;
     import org.springframework.dao.DataAccessException;
     
    @@ -17,11 +20,16 @@ import java.io.IOException;
     import java.io.InputStream;
     import java.io.OutputStream;
     import java.io.Serializable;
    +import java.util.Arrays;
     import java.util.Properties;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import jenkins.security.HMACConfidentialKey;
     
    +import javax.annotation.Nonnull;
    +
    +import javax.annotation.CheckForNull;
    +
     /**
      * Represents the authentication credential store of the CLI client.
      *
    @@ -38,6 +46,9 @@ public class ClientAuthenticationCache implements Serializable {
     
         private static final HMACConfidentialKey MAC = new HMACConfidentialKey(ClientAuthenticationCache.class, "MAC");
         private static final Logger LOGGER = Logger.getLogger(ClientAuthenticationCache.class.getName());
    +    private static final String VERIFICATION_FRAGMENT_SEPARATOR = "_";
    +    private static final String USERNAME_VERIFICATION_SEPARATOR = ":";
    +    private static final String VERSION_2 = "v2";
     
         /**
          * Where the store should be placed.
    @@ -51,16 +62,7 @@ public class ClientAuthenticationCache implements Serializable {
         final Properties props = new Properties();
     
         public ClientAuthenticationCache(Channel channel) throws IOException, InterruptedException {
    -        store = (channel==null ? FilePath.localChannel :  channel).call(new MasterToSlaveCallable<FilePath, IOException>() {
    -            public FilePath call() throws IOException {
    -                File home = new File(System.getProperty("user.home"));
    -                File hudsonHome = new File(home, ".hudson");
    -                if (hudsonHome.exists()) {
    -                    return new FilePath(new File(hudsonHome, "cli-credentials"));
    -                }
    -                return new FilePath(new File(home, ".jenkins/cli-credentials"));
    -            }
    -        });
    +        store = (channel==null ? FilePath.localChannel :  channel).call(new CredentialsFilePathMasterToSlaveCallable());
             if (store.exists()) {
                 try (InputStream istream = store.read()) {
                     props.load(istream);
    @@ -73,8 +75,7 @@ public class ClientAuthenticationCache implements Serializable {
          *
          * @return {@link jenkins.model.Jenkins#ANONYMOUS} if no such credential is found, or if the stored credential is invalid.
          */
    -    public Authentication get() {
    -        Jenkins h = Jenkins.getActiveInstance();
    +    public @Nonnull Authentication get() {
             String val = props.getProperty(getPropertyKey());
             if (val == null) {
                 LOGGER.finer("No stored CLI authentication");
    @@ -85,21 +86,101 @@ public class ClientAuthenticationCache implements Serializable {
                 LOGGER.log(Level.FINE, "Ignoring insecure stored CLI authentication for {0}", oldSecret.getPlainText());
                 return Jenkins.ANONYMOUS;
             }
    -        int idx = val.lastIndexOf(':');
    +        int idx = val.lastIndexOf(USERNAME_VERIFICATION_SEPARATOR);
             if (idx == -1) {
                 LOGGER.log(Level.FINE, "Ignoring malformed stored CLI authentication: {0}", val);
                 return Jenkins.ANONYMOUS;
             }
             String username = val.substring(0, idx);
    -        if (!MAC.checkMac(username, val.substring(idx + 1))) {
    -            LOGGER.log(Level.FINE, "Ignoring stored CLI authentication due to MAC mismatch: {0}", val);
    +        String verificationPart = val.substring(idx + 1);
    +        int indexOfSeparator = verificationPart.indexOf(VERIFICATION_FRAGMENT_SEPARATOR);
    +        if (indexOfSeparator == -1) {
    +            return legacy(username, verificationPart, val);
    +        }
    +        
    +        /*
    +         * Format of the cache data: [username]:[verificationToken]
    +         * Where the verificationToken is: [mac]_[version]_[restOfFragments]
    +         */
    +
    +        String[] verificationFragments = verificationPart.split(VERIFICATION_FRAGMENT_SEPARATOR);
    +        if (verificationFragments.length < 2) {
    +            LOGGER.log(Level.FINE, "Ignoring malformed stored CLI authentication verification: {0}", val);
    +            return Jenkins.ANONYMOUS;
    +        }
    +
    +        // the mac is only verifying the username
    +        String macFragment = verificationFragments[0];
    +        String version = verificationFragments[1];
    +        String[] restOfFragments = Arrays.copyOfRange(verificationFragments, 2, verificationFragments.length);
    +
    +        Authentication authFromVersion;
    +        if (VERSION_2.equals(version)) {
    +            authFromVersion = version2(username, restOfFragments, val);
    +        } else {
    +            LOGGER.log(Level.FINE, "Unrecognized version for stored CLI authentication verification: {0}", val);
    +            return Jenkins.ANONYMOUS;
    +        }
    +
    +        if (authFromVersion != null) {
    +            return authFromVersion;
    +        }
    +
    +        return getUserAuthIfValidMac(username, macFragment, val);
    +    }
    +    
    +    private Authentication legacy(String username, String mac, String fullValueStored){
    +        return getUserAuthIfValidMac(username, mac, fullValueStored);
    +    }
    +
    +    /**
    +     * restOfFragments format: [userSeed]
    +     * 
    +     * @return {@code null} when the method wants to let the default behavior to proceed
    +     */
    +    private @CheckForNull Authentication version2(String username, String[] restOfFragments, String fullValueStored){
    +        if (restOfFragments.length != 1) {
    +            LOGGER.log(Level.FINE, "Number of fragments invalid for stored CLI authentication verification: {0}", fullValueStored);
    +            return Jenkins.ANONYMOUS;
    +        }
    +
    +        if (UserSeedProperty.DISABLE_USER_SEED) {
    +            return null;
    +        }
    +
    +        User user = User.getById(username, false);
    +        if (user == null) {
    +            LOGGER.log(Level.FINE, "User not found for stored CLI authentication verification: {0}", fullValueStored);
    +            return Jenkins.ANONYMOUS;
    +        }
    +
    +        UserSeedProperty property = user.getProperty(UserSeedProperty.class);
    +        if (property == null) {
    +            LOGGER.log(Level.INFO, "User does not have a user seed but one is contained in CLI authentication: {0}", fullValueStored);
    +            return Jenkins.ANONYMOUS;
    +        }
    +
    +        String receivedUserSeed = restOfFragments[0];
    +        String actualUserSeed = property.getSeed();
    +        if (!receivedUserSeed.equals(actualUserSeed)) {
    +            LOGGER.log(Level.FINE, "Actual user seed does not correspond to the one in stored CLI authentication: {0}", fullValueStored);
    +            return Jenkins.ANONYMOUS;
    +        }
    +
    +        return null;
    +    }
    +    
    +    private Authentication getUserAuthIfValidMac(String username, String mac, String fullValueStored) {
    +        if (!MAC.checkMac(username, mac)) {
    +            LOGGER.log(Level.FINE, "Ignoring stored CLI authentication due to MAC mismatch: {0}", fullValueStored);
                 return Jenkins.ANONYMOUS;
             }
             try {
    -            UserDetails u = h.getSecurityRealm().loadUserByUsername(username);
    +            UserDetails u = Jenkins.get().getSecurityRealm().loadUserByUsername(username);
                 LOGGER.log(Level.FINER, "Loaded stored CLI authentication for {0}", username);
                 return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities());
             } catch (AuthenticationException | DataAccessException e) {
    +            //TODO there is no check to be consistent with User.ALLOW_NON_EXISTENT_USER_TO_LOGIN
                 LOGGER.log(Level.FINE, "Stored CLI authentication did not correspond to a valid user: " + username, e);
                 return Jenkins.ANONYMOUS;
             }
    @@ -110,9 +191,11 @@ public class ClientAuthenticationCache implements Serializable {
          */
         @VisibleForTesting
         String getPropertyKey() {
    -        String url = Jenkins.getActiveInstance().getRootUrl();
    +        Jenkins j = Jenkins.getActiveInstance();
    +        String url = j.getRootUrl();
             if (url!=null)  return url;
    -        return Secret.fromString("key").getEncryptedValue();
    +        
    +        return j.getLegacyInstanceId();
         }
     
         /**
    @@ -125,11 +208,48 @@ public class ClientAuthenticationCache implements Serializable {
             // as it's not required.
             UserDetails u = h.getSecurityRealm().loadUserByUsername(a.getName());
             String username = u.getUsername();
    -        props.setProperty(getPropertyKey(), username + ":" + MAC.mac(username));
    +
    +        User user;
    +        if (a instanceof AnonymousAuthenticationToken) {
    +            user = null;
    +        } else {
    +            user = User.getById(a.getName(), false);
    +        }
    +
    +        if (user == null) {
    +            // anonymous case or user not existing case, but normally should not occur
    +            // since the only call to it is by LoginCommand after a non-anonymous login.
    +            setUsingLegacyMethod(username);
    +            return;
    +        }
    +
    +        String userSeed;
    +        UserSeedProperty userSeedProperty = user.getProperty(UserSeedProperty.class);
    +        if (userSeedProperty == null) {
    +            userSeed = "no-user-seed";
    +        } else {
    +            userSeed = userSeedProperty.getSeed();
    +        }
    +        String mac = getMacOf(username);
    +        String validationFragment = String.join(VERIFICATION_FRAGMENT_SEPARATOR, mac, VERSION_2, userSeed);
    +
    +        String propertyValue = username + USERNAME_VERIFICATION_SEPARATOR + validationFragment;
    +        props.setProperty(getPropertyKey(), propertyValue);
     
             save();
         }
     
    +    @VisibleForTesting
    +    void setUsingLegacyMethod(String username) throws IOException, InterruptedException {
    +        props.setProperty(getPropertyKey(), username + USERNAME_VERIFICATION_SEPARATOR + getMacOf(username));
    +        save();
    +    }
    +    
    +    @VisibleForTesting
    +    @Nonnull String getMacOf(@Nonnull String value){
    +        return MAC.mac(value);
    +    }
    +
         /**
          * Removes the persisted credential, if there's one.
          */
    @@ -146,4 +266,15 @@ public class ClientAuthenticationCache implements Serializable {
             // try to protect this file from other users, if we can.
             store.chmod(0600);
         }
    +
    +    private static class CredentialsFilePathMasterToSlaveCallable extends MasterToSlaveCallable<FilePath, IOException> {
    +        public FilePath call() throws IOException {
    +            File home = new File(System.getProperty("user.home"));
    +            File hudsonHome = new File(home, ".hudson");
    +            if (hudsonHome.exists()) {
    +                return new FilePath(new File(hudsonHome, "cli-credentials"));
    +            }
    +            return new FilePath(new File(home, ".jenkins/cli-credentials"));
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/cli/ConnectNodeCommand.java b/core/src/main/java/hudson/cli/ConnectNodeCommand.java
    index ae20f5e60848507503cca9f3ff0d3a21cb9c53a5..189d2b5406376a2e88c6411308aa991238d5339e 100644
    --- a/core/src/main/java/hudson/cli/ConnectNodeCommand.java
    +++ b/core/src/main/java/hudson/cli/ConnectNodeCommand.java
    @@ -62,7 +62,7 @@ public class ConnectNodeCommand extends CLICommand {
             boolean errorOccurred = false;
             final Jenkins jenkins = Jenkins.getActiveInstance();
     
    -        final HashSet<String> hs = new HashSet<String>();
    +        final HashSet<String> hs = new HashSet<>();
             hs.addAll(nodes);
     
             List<String> names = null;
    diff --git a/core/src/main/java/hudson/cli/ConsoleCommand.java b/core/src/main/java/hudson/cli/ConsoleCommand.java
    index f98fdc95a29594a98fb4da2f0d81bb3aeac36215..8965af0fe6957553ebae2bc57c7fd2aa1d8d4a66 100644
    --- a/core/src/main/java/hudson/cli/ConsoleCommand.java
    +++ b/core/src/main/java/hudson/cli/ConsoleCommand.java
    @@ -41,7 +41,7 @@ public class ConsoleCommand extends CLICommand {
         public int n = -1;
     
         protected int run() throws Exception {
    -        job.checkPermission(Item.BUILD);
    +        job.checkPermission(Item.READ);
     
             Run<?,?> run;
     
    diff --git a/core/src/main/java/hudson/cli/CreateNodeCommand.java b/core/src/main/java/hudson/cli/CreateNodeCommand.java
    index 6a7f6dee7698cf6f5f4dddeb2167dd5df003a51f..edab3f7e71ec2eda866dd7c12b32c1920d902c81 100644
    --- a/core/src/main/java/hudson/cli/CreateNodeCommand.java
    +++ b/core/src/main/java/hudson/cli/CreateNodeCommand.java
    @@ -32,7 +32,6 @@ import hudson.model.User;
     import jenkins.model.Jenkins;
     
     import org.kohsuke.args4j.Argument;
    -import org.kohsuke.args4j.CmdLineException;
     
     /**
      * @author ogondza
    diff --git a/core/src/main/java/hudson/cli/DeleteBuildsCommand.java b/core/src/main/java/hudson/cli/DeleteBuildsCommand.java
    index db86c433ab538a8317058766afdc4f4d39dd93b5..b7c2531446b0c21462ab81f0863da28d6592a648 100644
    --- a/core/src/main/java/hudson/cli/DeleteBuildsCommand.java
    +++ b/core/src/main/java/hudson/cli/DeleteBuildsCommand.java
    @@ -31,14 +31,14 @@ import java.io.PrintStream;
     import java.util.HashSet;
     import java.util.List;
     import org.kohsuke.accmod.Restricted;
    -import org.kohsuke.accmod.restrictions.DoNotUse;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
      * Deletes builds records in a bulk.
      *
      * @author Kohsuke Kawaguchi
      */
    -@Restricted(DoNotUse.class) // command implementation only
    +@Restricted(NoExternalUse.class) // command implementation only
     @Extension
     public class DeleteBuildsCommand extends RunRangeCommand {
         @Override
    @@ -57,7 +57,7 @@ public class DeleteBuildsCommand extends RunRangeCommand {
         protected int act(List<Run<?, ?>> builds) throws IOException {
             job.checkPermission(Run.DELETE);
     
    -        final HashSet<Integer> hsBuilds = new HashSet<Integer>();
    +        final HashSet<Integer> hsBuilds = new HashSet<>();
     
             for (Run<?, ?> build : builds) {
                 if (!hsBuilds.contains(build.number)) {
    diff --git a/core/src/main/java/hudson/cli/DeleteJobCommand.java b/core/src/main/java/hudson/cli/DeleteJobCommand.java
    index 199296f87fc48d09a14d289ef4c36cea8f05901f..03e63e8579da3f9b631577120b30bb873ab6d8ae 100644
    --- a/core/src/main/java/hudson/cli/DeleteJobCommand.java
    +++ b/core/src/main/java/hudson/cli/DeleteJobCommand.java
    @@ -31,7 +31,6 @@ import org.kohsuke.args4j.Argument;
     
     import java.util.List;
     import java.util.HashSet;
    -import java.util.logging.Logger;
     
     /**
      * CLI command, which deletes a job or multiple jobs.
    @@ -56,7 +55,7 @@ public class DeleteJobCommand extends CLICommand {
             boolean errorOccurred = false;
             final Jenkins jenkins = Jenkins.getActiveInstance();
     
    -        final HashSet<String> hs = new HashSet<String>();
    +        final HashSet<String> hs = new HashSet<>();
             hs.addAll(jobs);
     
             for (String job_s: hs) {
    diff --git a/core/src/main/java/hudson/cli/DeleteNodeCommand.java b/core/src/main/java/hudson/cli/DeleteNodeCommand.java
    index 03001fccc3a10886fb164632952e426d31862ba7..60a8821e0f7134b396344ce4fc6078dd4c5eac24 100644
    --- a/core/src/main/java/hudson/cli/DeleteNodeCommand.java
    +++ b/core/src/main/java/hudson/cli/DeleteNodeCommand.java
    @@ -31,7 +31,6 @@ import org.kohsuke.args4j.Argument;
     
     import java.util.HashSet;
     import java.util.List;
    -import java.util.logging.Logger;
     
     /**
      * CLI command, which deletes Jenkins nodes.
    @@ -56,7 +55,7 @@ public class DeleteNodeCommand extends CLICommand {
             boolean errorOccurred = false;
             final Jenkins jenkins = Jenkins.getActiveInstance();
     
    -        final HashSet<String> hs = new HashSet<String>();
    +        final HashSet<String> hs = new HashSet<>();
             hs.addAll(nodes);
     
             for (String node_s : hs) {
    diff --git a/core/src/main/java/hudson/cli/DeleteViewCommand.java b/core/src/main/java/hudson/cli/DeleteViewCommand.java
    index 894978e683fcac696a634fcad383c199e5dec840..955a476ad96c009dac15484c864e180b8f337ce0 100644
    --- a/core/src/main/java/hudson/cli/DeleteViewCommand.java
    +++ b/core/src/main/java/hudson/cli/DeleteViewCommand.java
    @@ -33,7 +33,6 @@ import org.kohsuke.args4j.Argument;
     
     import java.util.HashSet;
     import java.util.List;
    -import java.util.logging.Logger;
     
     /**
      * @author ogondza, pjanouse
    @@ -57,7 +56,7 @@ public class DeleteViewCommand extends CLICommand {
             boolean errorOccurred = false;
     
             // Remove duplicates
    -        final HashSet<String> hs = new HashSet<String>();
    +        final HashSet<String> hs = new HashSet<>();
             hs.addAll(views);
     
             ViewOptionHandler voh = new ViewOptionHandler(null, null, null);
    diff --git a/core/src/main/java/hudson/cli/DisablePluginCommand.java b/core/src/main/java/hudson/cli/DisablePluginCommand.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..354d69a561f8ed44b6584e43f7a53f74cfee713f
    --- /dev/null
    +++ b/core/src/main/java/hudson/cli/DisablePluginCommand.java
    @@ -0,0 +1,236 @@
    +/*
    + * The MIT License
    + *
    + * Copyright (c) 2018 CloudBees, Inc.
    + *
    + * Permission is hereby granted, free of charge, to any person obtaining a copy
    + * of this software and associated documentation files (the "Software"), to deal
    + * in the Software without restriction, including without limitation the rights
    + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    + * copies of the Software, and to permit persons to whom the Software is
    + * furnished to do so, subject to the following conditions:
    + *
    + * The above copyright notice and this permission notice shall be included in
    + * all copies or substantial portions of the Software.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    + * THE SOFTWARE.
    + */
    +
    +package hudson.cli;
    +
    +import hudson.Extension;
    +import hudson.PluginWrapper;
    +import hudson.lifecycle.RestartNotSupportedException;
    +import jenkins.model.Jenkins;
    +import org.kohsuke.args4j.Argument;
    +import org.kohsuke.args4j.Option;
    +
    +import java.io.PrintStream;
    +import java.util.List;
    +
    +/**
    + * Disable one or more installed plugins.
    + * @since 2.151
    + */
    +@Extension
    +public class DisablePluginCommand extends CLICommand {
    +
    +    @Argument(metaVar = "plugin1 plugin2 plugin3", required = true, usage = "Plugins to be disabled.")
    +    private List<String> pluginNames;
    +
    +    @Option(name = "-restart", aliases = "-r", usage = "Restart Jenkins after disabling plugins.")
    +    private boolean restart;
    +
    +    @Option(name = "-strategy", aliases = "-s", metaVar = "strategy", usage = "How to process the dependant plugins. \n" +
    +            "- none: if a mandatory dependant plugin exists and it is enabled, the plugin cannot be disabled (default value).\n" +
    +            "- mandatory: all mandatory dependant plugins are also disabled, optional dependant plugins remain enabled.\n" +
    +            "- all: all dependant plugins are also disabled, no matter if its dependency is optional or mandatory.")
    +    private String strategy = PluginWrapper.PluginDisableStrategy.NONE.toString();
    +
    +    @Option(name = "-quiet", aliases = "-q", usage = "Be quiet, print only the error messages")
    +    private boolean quiet;
    +
    +    private static final int INDENT_SPACE = 3;
    +
    +    @Override
    +    public String getShortDescription() {
    +        return Messages.DisablePluginCommand_ShortDescription();
    +    }
    +
    +    // package-private access to be able to use it in the tests
    +    static final int RETURN_CODE_NOT_DISABLED_DEPENDANTS = 16;
    +    static final int RETURN_CODE_NO_SUCH_PLUGIN = 17;
    +
    +    @Override
    +    protected void printUsageSummary(PrintStream stderr) {
    +        super.printUsageSummary(stderr);
    +        stderr.println(Messages.DisablePluginCommand_PrintUsageSummary());
    +    }
    +
    +    @Override
    +    protected int run() throws Exception {
    +        Jenkins jenkins = Jenkins.get();
    +        jenkins.checkPermission(Jenkins.ADMINISTER);
    +
    +        // get the strategy as an enum
    +        PluginWrapper.PluginDisableStrategy strategyToUse;
    +        try {
    +            strategyToUse = PluginWrapper.PluginDisableStrategy.valueOf(strategy.toUpperCase());
    +        } catch (IllegalArgumentException iae) {
    +            throw new IllegalArgumentException(hudson.cli.Messages.DisablePluginCommand_NoSuchStrategy(strategy, String.format("%s, %s, %s", PluginWrapper.PluginDisableStrategy.NONE, PluginWrapper.PluginDisableStrategy.MANDATORY, PluginWrapper.PluginDisableStrategy.ALL)));
    +        }
    +
    +        // disable...
    +        List<PluginWrapper.PluginDisableResult> results = jenkins.pluginManager.disablePlugins(strategyToUse, pluginNames);
    +
    +        // print results ...
    +        printResults(results);
    +
    +        // restart if it was required and it's necessary (at least one plugin was disabled in this execution)
    +        restartIfNecessary(results);
    +
    +        // return the appropriate error code
    +        return getResultCode(results);
    +    }
    +
    +    /**
    +     * Print the result of all the process
    +     * @param results the list of results for the disablement of each plugin
    +     */
    +    private void printResults(List<PluginWrapper.PluginDisableResult> results) {
    +        for (PluginWrapper.PluginDisableResult oneResult : results) {
    +            printResult(oneResult, 0);
    +        }
    +    }
    +
    +    /**
    +     * Print indented the arguments with the format passed beginning with the indent passed.
    +     * @param indent number of spaces at the beginning.
    +     * @param format format as in {@link String#format(String, Object...)}
    +     * @param arguments arguments to print as in {@link String#format(String, Object...)}
    +     */
    +    private void printIndented(int indent, String format, String... arguments) {
    +        if (indent == 0) {
    +            stdout.format(format + "%n", (Object[]) arguments);
    +        } else {
    +            String[] newArgs = new String[arguments.length + 1];
    +            newArgs[0] = " ";
    +            System.arraycopy(arguments, 0, newArgs, 1, arguments.length);
    +
    +            String f = "%" + indent + "s" + format + "%n";
    +            stdout.format(f, newArgs);
    +        }
    +    }
    +
    +    /**
    +     * Print the result of a plugin disablement with the indent passed.
    +     * @param oneResult the result of the disablement of a plugin.
    +     * @param indent the initial indent.
    +     */
    +    private void printResult(PluginWrapper.PluginDisableResult oneResult, int indent) {
    +        PluginWrapper.PluginDisableStatus status = oneResult.getStatus();
    +        if (quiet && (PluginWrapper.PluginDisableStatus.DISABLED.equals(status) || PluginWrapper.PluginDisableStatus.ALREADY_DISABLED.equals(status))) {
    +            return;
    +        }
    +
    +        printIndented(indent, Messages.DisablePluginCommand_StatusMessage(oneResult.getPlugin(), oneResult.getStatus(), oneResult.getMessage()));
    +        if (oneResult.getDependantsDisableStatus().size() > 0) {
    +            indent += INDENT_SPACE;
    +            for (PluginWrapper.PluginDisableResult oneDependantResult : oneResult.getDependantsDisableStatus()) {
    +                printResult(oneDependantResult, indent);
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Restart if at least one plugin was disabled in this process.
    +     * @param results the list of results after the disablement of the plugins.
    +     */
    +    private void restartIfNecessary(List<PluginWrapper.PluginDisableResult> results) throws RestartNotSupportedException {
    +        if (restart) {
    +            for (PluginWrapper.PluginDisableResult oneResult : results) {
    +                if (restartIfNecessary(oneResult)) {
    +                    break;
    +                }
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Restart if this particular result of the disablement of a plugin and its dependant plugins (depending on the
    +     * strategy used) has a plugin disablexd.
    +     * @param oneResult the result of a plugin (and its dependants).
    +     * @return true if it end up in restarting jenkins.
    +     */
    +    private boolean restartIfNecessary(PluginWrapper.PluginDisableResult oneResult) throws RestartNotSupportedException {
    +        PluginWrapper.PluginDisableStatus status = oneResult.getStatus();
    +        if (PluginWrapper.PluginDisableStatus.DISABLED.equals(status)) {
    +            Jenkins.get().safeRestart();
    +            return true;
    +        }
    +
    +        if (oneResult.getDependantsDisableStatus().size() > 0) {
    +            for (PluginWrapper.PluginDisableResult oneDependantResult : oneResult.getDependantsDisableStatus()) {
    +                if (restartIfNecessary(oneDependantResult)) {
    +                    return true;
    +                }
    +            }
    +        }
    +
    +        return false;
    +    }
    +
    +
    +    /**
    +     * Calculate the result code of the full process based in what went on during the process
    +     * @param results he list of results for the disablement of each plugin
    +     * @return the status code. 0 if all plugins disabled. {@link #RETURN_CODE_NOT_DISABLED_DEPENDANTS} if some
    +     * dependant plugin is not disabled (with strategy NONE), {@link #RETURN_CODE_NO_SUCH_PLUGIN} if some passed
    +     * plugin doesn't exist. Whatever happens first.
    +     */
    +    private int getResultCode(List<PluginWrapper.PluginDisableResult> results) {
    +        int result = 0;
    +        for (PluginWrapper.PluginDisableResult oneResult : results) {
    +            result = getResultCode(oneResult);
    +            if (result != 0) {
    +                break;
    +            }
    +        }
    +
    +        return result;
    +    }
    +
    +    /**
    +     * Calculate the result code of the disablement of one plugin based in what went on during the process of this one
    +     * and its dependant plugins.
    +     * @param result the result of the disablement of this plugin
    +     * @return the status code
    +     */
    +    private int getResultCode(PluginWrapper.PluginDisableResult result) {
    +        int returnCode = 0;
    +        switch (result.getStatus()){
    +            case NOT_DISABLED_DEPENDANTS:
    +                returnCode = RETURN_CODE_NOT_DISABLED_DEPENDANTS;
    +                break;
    +            case NO_SUCH_PLUGIN:
    +                returnCode = RETURN_CODE_NO_SUCH_PLUGIN;
    +        }
    +
    +        if (returnCode == 0) {
    +            for (PluginWrapper.PluginDisableResult oneDependantResult : result.getDependantsDisableStatus()) {
    +                returnCode = getResultCode(oneDependantResult);
    +                if (returnCode != 0) {
    +                    break;
    +                }
    +            }
    +        }
    +
    +        return returnCode;
    +    }
    +}
    diff --git a/core/src/main/java/hudson/cli/DisconnectNodeCommand.java b/core/src/main/java/hudson/cli/DisconnectNodeCommand.java
    index 65c95106eb5f8653007b8d62bf8aa5c8c5e987e0..ad003521d77a2025ad09eb5f9e16d6cc06e7f3bb 100644
    --- a/core/src/main/java/hudson/cli/DisconnectNodeCommand.java
    +++ b/core/src/main/java/hudson/cli/DisconnectNodeCommand.java
    @@ -61,7 +61,7 @@ public class DisconnectNodeCommand extends CLICommand {
             boolean errorOccurred = false;
             final Jenkins jenkins = Jenkins.getActiveInstance();
     
    -        final HashSet<String> hs = new HashSet<String>();
    +        final HashSet<String> hs = new HashSet<>();
             hs.addAll(nodes);
     
             List<String> names = null;
    diff --git a/core/src/main/java/hudson/cli/EnablePluginCommand.java b/core/src/main/java/hudson/cli/EnablePluginCommand.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..fcee649c03d8d1d4ad605a656d835296a2e16b04
    --- /dev/null
    +++ b/core/src/main/java/hudson/cli/EnablePluginCommand.java
    @@ -0,0 +1,102 @@
    +/*
    + * The MIT License
    + *
    + * Copyright (c) 2018 CloudBees, Inc.
    + *
    + * Permission is hereby granted, free of charge, to any person obtaining a copy
    + * of this software and associated documentation files (the "Software"), to deal
    + * in the Software without restriction, including without limitation the rights
    + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    + * copies of the Software, and to permit persons to whom the Software is
    + * furnished to do so, subject to the following conditions:
    + *
    + * The above copyright notice and this permission notice shall be included in
    + * all copies or substantial portions of the Software.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    + * THE SOFTWARE.
    + */
    +
    +package hudson.cli;
    +
    +import hudson.Extension;
    +import hudson.PluginManager;
    +import hudson.PluginWrapper;
    +import jenkins.model.Jenkins;
    +import org.kohsuke.args4j.Argument;
    +import org.kohsuke.args4j.Option;
    +
    +import java.io.IOException;
    +import java.util.List;
    +
    +/**
    + * Enables one or more installed plugins. The listed plugins must already be installed along with its dependencies.
    + * Any listed plugin with disabled dependencies will have its dependencies enabled transitively. Note that enabling an
    + * already enabled plugin does nothing.
    + *
    + * @since 2.136
    + */
    +@Extension
    +public class EnablePluginCommand extends CLICommand {
    +
    +    @Argument(required = true, usage = "Enables the plugins with the given short names and their dependencies.")
    +    private List<String> pluginNames;
    +
    +    @Option(name = "-restart", usage = "Restart Jenkins after enabling plugins.")
    +    private boolean restart;
    +
    +    @Override
    +    public String getShortDescription() {
    +        return Messages.EnablePluginCommand_ShortDescription();
    +    }
    +
    +    @Override
    +    protected int run() throws Exception {
    +        Jenkins jenkins = Jenkins.get();
    +        jenkins.checkPermission(Jenkins.ADMINISTER);
    +        PluginManager manager = jenkins.getPluginManager();
    +        boolean enabledAnyPlugins = false;
    +        for (String pluginName : pluginNames) {
    +            enabledAnyPlugins |= enablePlugin(manager, pluginName);
    +        }
    +        if (restart && enabledAnyPlugins) {
    +            jenkins.safeRestart();
    +        }
    +        return 0;
    +    }
    +
    +    private boolean enablePlugin(PluginManager manager, String shortName) throws IOException {
    +        PluginWrapper plugin = manager.getPlugin(shortName);
    +        if (plugin == null) {
    +            throw new IllegalArgumentException(Messages.EnablePluginCommand_NoSuchPlugin(shortName));
    +        }
    +        if (plugin.isEnabled()) {
    +            return false;
    +        }
    +        stdout.println(String.format("Enabling plugin `%s' (%s)", plugin.getShortName(), plugin.getVersion()));
    +        enableDependencies(manager, plugin);
    +        plugin.enable();
    +        stdout.println(String.format("Plugin `%s' was enabled.", plugin.getShortName()));
    +        return true;
    +    }
    +
    +    private void enableDependencies(PluginManager manager, PluginWrapper plugin) throws IOException {
    +        for (PluginWrapper.Dependency dep : plugin.getDependencies()) {
    +            PluginWrapper dependency = manager.getPlugin(dep.shortName);
    +            if (dependency == null) {
    +                throw new IllegalArgumentException(Messages.EnablePluginCommand_MissingDependencies(plugin.getShortName(), dep));
    +            }
    +            if (!dependency.isEnabled()) {
    +                enableDependencies(manager, dependency);
    +                stdout.println(String.format("Enabling plugin dependency `%s' (%s) for `%s'", dependency.getShortName(), dependency.getVersion(), plugin.getShortName()));
    +                dependency.enable();
    +            }
    +        }
    +    }
    +
    +}
    diff --git a/core/src/main/java/hudson/cli/GroovyCommand.java b/core/src/main/java/hudson/cli/GroovyCommand.java
    index e7b247951650711c23cffb22d1bc0273dc78e0f1..5e27f5478199f5f05c6d132e8b1f351e361cb7e6 100644
    --- a/core/src/main/java/hudson/cli/GroovyCommand.java
    +++ b/core/src/main/java/hudson/cli/GroovyCommand.java
    @@ -59,7 +59,7 @@ public class GroovyCommand extends CLICommand {
          * Remaining arguments.
          */
         @Argument(metaVar="ARGUMENTS", index=1, usage="Command line arguments to pass into script.")
    -    public List<String> remaining = new ArrayList<String>();
    +    public List<String> remaining = new ArrayList<>();
     
         protected int run() throws Exception {
             // this allows the caller to manipulate the JVM state, so require the execute script privilege.
    diff --git a/core/src/main/java/hudson/cli/GroovyshCommand.java b/core/src/main/java/hudson/cli/GroovyshCommand.java
    index 31f998d0aa5be777ef55f2d4c1bfbf3aab4fe459..6bd34dc6911db8932473be2aa8f02a9b55bc523a 100644
    --- a/core/src/main/java/hudson/cli/GroovyshCommand.java
    +++ b/core/src/main/java/hudson/cli/GroovyshCommand.java
    @@ -56,7 +56,7 @@ public class GroovyshCommand extends CLICommand {
             return Messages.GroovyshCommand_ShortDescription();
         }
     
    -    @Argument(metaVar="ARGS") public List<String> args = new ArrayList<String>();
    +    @Argument(metaVar="ARGS") public List<String> args = new ArrayList<>();
     
         @Override
         protected int run() {
    diff --git a/core/src/main/java/hudson/cli/HelpCommand.java b/core/src/main/java/hudson/cli/HelpCommand.java
    index 60fcc0970be1f6faa8badffcdfa0b1dde132a42c..071bce6a7fc49d9f400dfec4198d4a2acf04a796 100644
    --- a/core/src/main/java/hudson/cli/HelpCommand.java
    +++ b/core/src/main/java/hudson/cli/HelpCommand.java
    @@ -53,7 +53,7 @@ public class HelpCommand extends CLICommand {
         protected int run() throws Exception {
             if (!Jenkins.getActiveInstance().hasPermission(Jenkins.READ)) {
                 throw new AccessDeniedException("You must authenticate to access this Jenkins.\n"
    -                    + hudson.cli.client.Messages.CLI_Usage());
    +                    + CLI.usage());
             }
     
             if (command != null)
    @@ -65,7 +65,7 @@ public class HelpCommand extends CLICommand {
         }
     
         private int showAllCommands() {
    -        Map<String,CLICommand> commands = new TreeMap<String,CLICommand>();
    +        Map<String,CLICommand> commands = new TreeMap<>();
             for (CLICommand c : CLICommand.all())
                 commands.put(c.getName(),c);
     
    diff --git a/core/src/main/java/hudson/cli/InstallPluginCommand.java b/core/src/main/java/hudson/cli/InstallPluginCommand.java
    index e04032893625736e4544ece5ba787a09172b0e17..95f1e55f7782124bb2bc0b4692e8aa9d6455d19e 100644
    --- a/core/src/main/java/hudson/cli/InstallPluginCommand.java
    +++ b/core/src/main/java/hudson/cli/InstallPluginCommand.java
    @@ -27,6 +27,7 @@ import hudson.AbortException;
     import hudson.Extension;
     import hudson.FilePath;
     import hudson.PluginManager;
    +import hudson.util.VersionNumber;
     import jenkins.model.Jenkins;
     import hudson.model.UpdateSite;
     import hudson.model.UpdateSite.Data;
    @@ -35,7 +36,6 @@ import org.kohsuke.args4j.Argument;
     import org.kohsuke.args4j.Option;
     
     import java.io.File;
    -import java.io.IOException;
     import java.net.URL;
     import java.net.MalformedURLException;
     import java.util.HashSet;
    @@ -60,7 +60,9 @@ public class InstallPluginCommand extends CLICommand {
                 "If this is an URL, Jenkins downloads the URL and installs that as a plugin. " +
                 "If it is the string ‘=’, the file will be read from standard input of the command, and ‘-name’ must be specified. " +
                 "Otherwise the name is assumed to be the short name of the plugin in the existing update center (like ‘findbugs’), " +
    -            "and the plugin will be installed from the update center.")
    +            "and the plugin will be installed from the update center. If the short name includes a minimum version number " +
    +            "(like ‘findbugs:1.4’), and there are multiple update centers publishing different versions, the update centers " +
    +            "will be searched in order for the first one publishing a version that is at least the specified version.")
         public List<String> sources = new ArrayList<String>();
     
         @Option(name="-name",usage="If specified, the plugin will be installed as this short name (whereas normally the name is inferred from the source name automatically).")
    @@ -133,7 +135,18 @@ public class InstallPluginCommand extends CLICommand {
                 }
     
                 // is this a plugin the update center?
    -            UpdateSite.Plugin p = h.getUpdateCenter().getPlugin(source);
    +            int index = source.lastIndexOf(':');
    +            UpdateSite.Plugin p;
    +            if (index == -1) {
    +                p = h.getUpdateCenter().getPlugin(source);
    +            } else {
    +                // try to find matching min version number
    +                VersionNumber version = new VersionNumber(source.substring(index + 1));
    +                p = h.getUpdateCenter().getPlugin(source.substring(0,index), version);
    +                if (p == null) {
    +                    p = h.getUpdateCenter().getPlugin(source);
    +                }
    +            }
                 if (p!=null) {
                     stdout.println(Messages.InstallPluginCommand_InstallingFromUpdateCenter(source));
                     Throwable e = p.deploy(dynamicLoad).get().getError();
    @@ -152,7 +165,7 @@ public class InstallPluginCommand extends CLICommand {
                     if (h.getUpdateCenter().getSites().isEmpty()) {
                         stdout.println(Messages.InstallPluginCommand_NoUpdateCenterDefined());
                     } else {
    -                    Set<String> candidates = new HashSet<String>();
    +                    Set<String> candidates = new HashSet<>();
                         for (UpdateSite s : h.getUpdateCenter().getSites()) {
                             Data dt = s.getData();
                             if (dt==null)
    diff --git a/core/src/main/java/hudson/cli/InstallToolCommand.java b/core/src/main/java/hudson/cli/InstallToolCommand.java
    index ef8db698f99df0f20da2a91c5092fc8804870d93..4b06b51686bf04a2b30a98f030733435a0aaa352 100644
    --- a/core/src/main/java/hudson/cli/InstallToolCommand.java
    +++ b/core/src/main/java/hudson/cli/InstallToolCommand.java
    @@ -78,11 +78,11 @@ public class InstallToolCommand extends CLICommand {
                 throw new IllegalStateException("No such job found: "+id.job);
             p.checkPermission(Item.CONFIGURE);
     
    -        List<String> toolTypes = new ArrayList<String>();
    +        List<String> toolTypes = new ArrayList<>();
             for (ToolDescriptor<?> d : ToolInstallation.all()) {
                 toolTypes.add(d.getDisplayName());
                 if (d.getDisplayName().equals(toolType)) {
    -                List<String> toolNames = new ArrayList<String>();
    +                List<String> toolNames = new ArrayList<>();
                     for (ToolInstallation t : d.getInstallations()) {
                         toolNames.add(t.getName());
                         if (t.getName().equals(toolName))
    diff --git a/core/src/main/java/hudson/cli/ListChangesCommand.java b/core/src/main/java/hudson/cli/ListChangesCommand.java
    index 8da20000734b65a56ab7eb9b6c326a85e443cf2e..1b398bddad9582c0bf01e94721eae9766213e21c 100644
    --- a/core/src/main/java/hudson/cli/ListChangesCommand.java
    +++ b/core/src/main/java/hudson/cli/ListChangesCommand.java
    @@ -15,14 +15,14 @@ import java.io.IOException;
     import java.io.PrintWriter;
     import java.util.List;
     import org.kohsuke.accmod.Restricted;
    -import org.kohsuke.accmod.restrictions.DoNotUse;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
      * Retrieves a change list for the specified builds.
      *
      * @author Kohsuke Kawaguchi
      */
    -@Restricted(DoNotUse.class) // command implementation only
    +@Restricted(NoExternalUse.class) // command implementation only
     @Extension
     public class ListChangesCommand extends RunRangeCommand {
         @Override
    diff --git a/core/src/main/java/hudson/cli/ListJobsCommand.java b/core/src/main/java/hudson/cli/ListJobsCommand.java
    index 45b9661e283768f21de570287777570bf4a294d6..8dae08e67470d1584384990287a8f34304a5aeff 100644
    --- a/core/src/main/java/hudson/cli/ListJobsCommand.java
    +++ b/core/src/main/java/hudson/cli/ListJobsCommand.java
    @@ -26,7 +26,6 @@ package hudson.cli;
     import java.util.Collection;
     
     import hudson.model.Item;
    -import hudson.model.Items;
     import hudson.model.TopLevelItem;
     import hudson.model.View;
     import hudson.Extension;
    @@ -66,7 +65,8 @@ public class ListJobsCommand extends CLICommand {
     
                     // If item group was found use it's jobs.
                     if (item instanceof ModifiableTopLevelItemGroup) {
    -                    jobs = Items.getAllItems((ModifiableTopLevelItemGroup) item, TopLevelItem.class);
    +                    jobs = ((ModifiableTopLevelItemGroup) item).getItems();
    +
                     }
                     // No view and no item group with the given name found.
                     else {
    diff --git a/core/src/main/java/hudson/cli/ListPluginsCommand.java b/core/src/main/java/hudson/cli/ListPluginsCommand.java
    index 99c926cdaf1ef8c648ff1a339b6fdf61314497f2..faa35dfa678897d72a0ed5306d419dadfaf0e296 100644
    --- a/core/src/main/java/hudson/cli/ListPluginsCommand.java
    +++ b/core/src/main/java/hudson/cli/ListPluginsCommand.java
    @@ -47,7 +47,9 @@ public class ListPluginsCommand extends CLICommand {
         public String name;
     
         protected int run() {
    -        Jenkins h = Jenkins.getActiveInstance();
    +        Jenkins h = Jenkins.getInstance();
    +        h.checkPermission(Jenkins.ADMINISTER);
    +        
             PluginManager pluginManager = h.getPluginManager();
     
             if (this.name != null) {
    diff --git a/core/src/main/java/hudson/cli/LoginCommand.java b/core/src/main/java/hudson/cli/LoginCommand.java
    index 8920ad41114313104b356c8e5d9687558befd9c1..55efd9dbcbb93633a4787a9d6c06496695bedd07 100644
    --- a/core/src/main/java/hudson/cli/LoginCommand.java
    +++ b/core/src/main/java/hudson/cli/LoginCommand.java
    @@ -3,6 +3,7 @@ package hudson.cli;
     import hudson.Extension;
     import java.io.PrintStream;
     import jenkins.model.Jenkins;
    +import jenkins.security.SecurityListener;
     import org.acegisecurity.Authentication;
     import org.kohsuke.args4j.CmdLineException;
     
    @@ -45,6 +46,8 @@ public class LoginCommand extends CLICommand {
             ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel());
             store.set(a);
     
    +        SecurityListener.fireLoggedIn(a.getName());
    +
             return 0;
         }
     
    diff --git a/core/src/main/java/hudson/cli/LogoutCommand.java b/core/src/main/java/hudson/cli/LogoutCommand.java
    index 822c99d84ef1747ec60365513d4e8cce9587126d..8578a6eb0486a289674f787ec9fa8d838d1c565e 100644
    --- a/core/src/main/java/hudson/cli/LogoutCommand.java
    +++ b/core/src/main/java/hudson/cli/LogoutCommand.java
    @@ -1,6 +1,9 @@
     package hudson.cli;
     
     import hudson.Extension;
    +import jenkins.security.SecurityListener;
    +import org.acegisecurity.Authentication;
    +
     import java.io.PrintStream;
     
     /**
    @@ -27,7 +30,13 @@ public class LogoutCommand extends CLICommand {
         @Override
         protected int run() throws Exception {
             ClientAuthenticationCache store = new ClientAuthenticationCache(checkChannel());
    +
    +        Authentication auth = store.get();
    +
             store.remove();
    +
    +        SecurityListener.fireLoggedOut(auth.getName());
    +
             return 0;
         }
     }
    diff --git a/core/src/main/java/hudson/cli/OfflineNodeCommand.java b/core/src/main/java/hudson/cli/OfflineNodeCommand.java
    index e003a633b28ce80895a5257e74973918c7ee9a2b..39270918dcb8446506a24386a5497c42eef98350 100644
    --- a/core/src/main/java/hudson/cli/OfflineNodeCommand.java
    +++ b/core/src/main/java/hudson/cli/OfflineNodeCommand.java
    @@ -34,7 +34,6 @@ import jenkins.model.Jenkins;
     import org.kohsuke.args4j.Argument;
     import org.kohsuke.args4j.Option;
     
    -import java.util.ArrayList;
     import java.util.HashSet;
     import java.util.List;
     
    @@ -60,8 +59,8 @@ public class OfflineNodeCommand extends CLICommand {
         @Override
         protected int run() throws Exception {
             boolean errorOccurred = false;
    -        final Jenkins jenkins = Jenkins.getInstance();
    -        final HashSet<String> hs = new HashSet<String>(nodes);
    +        final Jenkins jenkins = Jenkins.get();
    +        final HashSet<String> hs = new HashSet<>(nodes);
             List<String> names = null;
     
             for (String node_s : hs) {
    diff --git a/core/src/main/java/hudson/cli/OnlineNodeCommand.java b/core/src/main/java/hudson/cli/OnlineNodeCommand.java
    index 5cb190fcd8ccf6bdb2c7f4e3e4c7e55888ad6505..0594d3770c9702a954f732d611dce90283ddfb87 100644
    --- a/core/src/main/java/hudson/cli/OnlineNodeCommand.java
    +++ b/core/src/main/java/hudson/cli/OnlineNodeCommand.java
    @@ -56,7 +56,7 @@ public class OnlineNodeCommand extends CLICommand {
         protected int run() throws Exception {
             boolean errorOccurred = false;
             final Jenkins jenkins = Jenkins.getActiveInstance();
    -        final HashSet<String> hs = new HashSet<String>(nodes);
    +        final HashSet<String> hs = new HashSet<>(nodes);
             List<String> names = null;
     
             for (String node_s : hs) {
    diff --git a/core/src/main/java/hudson/cli/ReloadConfigurationCommand.java b/core/src/main/java/hudson/cli/ReloadConfigurationCommand.java
    index 25199ec5bed80421701fdbb9bb94537563b754a8..6b29dcf52904c09ff60c91ab014b395caeb8ab83 100644
    --- a/core/src/main/java/hudson/cli/ReloadConfigurationCommand.java
    +++ b/core/src/main/java/hudson/cli/ReloadConfigurationCommand.java
    @@ -25,7 +25,10 @@
     package hudson.cli;
     
     import hudson.Extension;
    +import hudson.util.HudsonIsLoading;
    +import hudson.util.JenkinsReloadFailed;
     import jenkins.model.Jenkins;
    +import org.kohsuke.stapler.WebApp;
     
     /**
      * Reload everything from the file system.
    @@ -43,8 +46,26 @@ public class ReloadConfigurationCommand extends CLICommand {
     
         @Override
         protected int run() throws Exception {
    -        Jenkins.getActiveInstance().doReload();
    -        return 0;
    +        Jenkins j = Jenkins.get();
    +        // Or perhaps simpler to inline the thread body of doReload?
    +        j.doReload();
    +        Object app;
    +        while ((app = WebApp.get(j.servletContext).getApp()) instanceof HudsonIsLoading) {
    +            Thread.sleep(100);
    +        }
    +        if (app instanceof Jenkins) {
    +            return 0;
    +        } else if (app instanceof JenkinsReloadFailed) {
    +            Throwable t = ((JenkinsReloadFailed) app).cause;
    +            if (t instanceof Exception) {
    +                throw (Exception) t;
    +            } else {
    +                throw new RuntimeException(t);
    +            }
    +        } else {
    +            stderr.println("Unexpected status " + app);
    +            return 1; // could throw JenkinsReloadFailed.cause if it were not deprecated
    +        }
         }
     
     }
    diff --git a/core/src/main/java/hudson/cli/ReloadJobCommand.java b/core/src/main/java/hudson/cli/ReloadJobCommand.java
    index c8849a317f4832ef5c8411f8fc3d29fffe3217ff..3e42d64e68660dc69d4075bbac82cceb638b1201 100644
    --- a/core/src/main/java/hudson/cli/ReloadJobCommand.java
    +++ b/core/src/main/java/hudson/cli/ReloadJobCommand.java
    @@ -26,11 +26,9 @@ package hudson.cli;
     import hudson.AbortException;
     import hudson.Extension;
     import hudson.model.AbstractItem;
    -import hudson.model.AbstractProject;
     import hudson.model.Item;
     
     import hudson.model.Items;
    -import hudson.model.TopLevelItem;
     import jenkins.model.Jenkins;
     import org.kohsuke.args4j.Argument;
     
    @@ -64,7 +62,7 @@ public class ReloadJobCommand extends CLICommand {
             boolean errorOccurred = false;
             final Jenkins jenkins = Jenkins.getActiveInstance();
     
    -        final HashSet<String> hs = new HashSet<String>();
    +        final HashSet<String> hs = new HashSet<>();
             hs.addAll(jobs);
     
             for (String job_s: hs) {
    diff --git a/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java b/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java
    index 786f33d21e1ee5fc1cb0d68cec7bcb8605816d40..5a543c91c53a399d398ed43f0cf8dfe92e9105b4 100644
    --- a/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java
    +++ b/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java
    @@ -31,7 +31,6 @@ import hudson.model.DirectlyModifiableView;
     import hudson.model.View;
     
     import org.kohsuke.args4j.Argument;
    -import org.kohsuke.args4j.CmdLineException;
     
     /**
      * @author ogondza
    diff --git a/core/src/main/java/hudson/cli/RunRangeCommand.java b/core/src/main/java/hudson/cli/RunRangeCommand.java
    index 2ba4b3b015da9a4c673459197071fab67e917ae8..ceb3ffc33f49120e678b727ac1df35b440b16c37 100644
    --- a/core/src/main/java/hudson/cli/RunRangeCommand.java
    +++ b/core/src/main/java/hudson/cli/RunRangeCommand.java
    @@ -10,7 +10,7 @@ import java.util.List;
     
     /**
      * {@link CLICommand} that acts on a series of {@link Run}s.
    - * @since FIXME
    + * @since 2.62
      */
     public abstract class RunRangeCommand extends CLICommand {
         @Argument(metaVar="JOB",usage="Name of the job to build",required=true,index=0)
    diff --git a/core/src/main/java/hudson/cli/declarative/CLIMethod.java b/core/src/main/java/hudson/cli/declarative/CLIMethod.java
    index d4e6c96ed4c2bf762a415b15297d9e764a7038ce..6366e4409ed667f116f64c06af8f37e9fd38d230 100644
    --- a/core/src/main/java/hudson/cli/declarative/CLIMethod.java
    +++ b/core/src/main/java/hudson/cli/declarative/CLIMethod.java
    @@ -38,8 +38,8 @@ import java.lang.annotation.Target;
      * Annotates methods on model objects to expose them as CLI commands.
      *
      * <p>
    - * You need to have <tt>Messages.properties</tt> in the same package with the
    - * <tt>CLI.<i>command-name</i>.shortDescription</tt> key to describe the command.
    + * You need to have {@code Messages.properties} in the same package with the
    + * {@code CLI.<i>command-name</i>.shortDescription} key to describe the command.
      * This is used for the same purpose as {@link CLICommand#getShortDescription()}.
      *
      * <p>
    diff --git a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java
    index 5868d62b9ad68a8f31a7980496a638b0ef611472..e0dccb1553daef04c119b53f26526da553ee8031 100644
    --- a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java
    +++ b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java
    @@ -47,6 +47,7 @@ import org.kohsuke.args4j.ClassParser;
     import org.kohsuke.args4j.CmdLineParser;
     import org.kohsuke.args4j.CmdLineException;
     
    +import javax.annotation.Nonnull;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.PrintStream;
    @@ -90,7 +91,7 @@ public class CLIRegisterer extends ExtensionFinder {
          * Finds a resolved method annotated with {@link CLIResolver}.
          */
         private Method findResolver(Class type) throws IOException {
    -        List<Method> resolvers = Util.filter(Index.list(CLIResolver.class, Jenkins.getInstance().getPluginManager().uberClassLoader), Method.class);
    +        List<Method> resolvers = Util.filter(Index.list(CLIResolver.class, Jenkins.get().getPluginManager().uberClassLoader), Method.class);
             for ( ; type!=null; type=type.getSuperclass())
                 for (Method m : resolvers)
                     if (m.getReturnType()==type)
    @@ -98,12 +99,12 @@ public class CLIRegisterer extends ExtensionFinder {
             return null;
         }
     
    -    private List<ExtensionComponent<CLICommand>> discover(final Jenkins hudson) {
    +    private List<ExtensionComponent<CLICommand>> discover(@Nonnull final Jenkins jenkins) {
             LOGGER.fine("Listing up @CLIMethod");
    -        List<ExtensionComponent<CLICommand>> r = new ArrayList<ExtensionComponent<CLICommand>>();
    +        List<ExtensionComponent<CLICommand>> r = new ArrayList<>();
     
             try {
    -            for ( final Method m : Util.filter(Index.list(CLIMethod.class, hudson.getPluginManager().uberClassLoader),Method.class)) {
    +            for ( final Method m : Util.filter(Index.list(CLIMethod.class, jenkins.getPluginManager().uberClassLoader),Method.class)) {
                     try {
                         // command name
                         final String name = m.getAnnotation(CLIMethod.class).name();
    @@ -111,7 +112,7 @@ public class CLIRegisterer extends ExtensionFinder {
                         final ResourceBundleHolder res = loadMessageBundle(m);
                         res.format("CLI."+name+".shortDescription");   // make sure we have the resource, to fail early
     
    -                    r.add(new ExtensionComponent<CLICommand>(new CloneableCLICommand() {
    +                    r.add(new ExtensionComponent<>(new CloneableCLICommand() {
                             @Override
                             public String getName() {
                                 return name;
    @@ -120,12 +121,12 @@ public class CLIRegisterer extends ExtensionFinder {
                             @Override
                             public String getShortDescription() {
                                 // format by using the right locale
    -                            return res.format("CLI."+name+".shortDescription");
    +                            return res.format("CLI." + name + ".shortDescription");
                             }
     
                             @Override
                             protected CmdLineParser getCmdLineParser() {
    -                            return bindMethod(new ArrayList<MethodBinder>());
    +                            return bindMethod(new ArrayList<>());
                             }
     
                             private CmdLineParser bindMethod(List<MethodBinder> binders) {
    @@ -134,7 +135,7 @@ public class CLIRegisterer extends ExtensionFinder {
                                 CmdLineParser parser = new CmdLineParser(null);
     
                                 //  build up the call sequence
    -                            Stack<Method> chains = new Stack<Method>();
    +                            Stack<Method> chains = new Stack<>();
                                 Method method = m;
                                 while (true) {
                                     chains.push(method);
    @@ -146,15 +147,15 @@ public class CLIRegisterer extends ExtensionFinder {
                                     try {
                                         method = findResolver(type);
                                     } catch (IOException ex) {
    -                                    throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type, ex);
    +                                    throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for " + type, ex);
                                     }
    -                                if (method==null) {
    -                                    throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type);
    +                                if (method == null) {
    +                                    throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for " + type);
                                     }
                                 }
     
                                 while (!chains.isEmpty())
    -                                binders.add(new MethodBinder(chains.pop(),this,parser));
    +                                binders.add(new MethodBinder(chains.pop(), this, parser));
     
                                 return parser;
                             }
    @@ -199,7 +200,7 @@ public class CLIRegisterer extends ExtensionFinder {
                                 this.stderr = stderr;
                                 this.locale = locale;
     
    -                            List<MethodBinder> binders = new ArrayList<MethodBinder>();
    +                            List<MethodBinder> binders = new ArrayList<>();
     
                                 CmdLineParser parser = bindMethod(binders);
                                 try {
    @@ -207,7 +208,7 @@ public class CLIRegisterer extends ExtensionFinder {
                                     Authentication old = sc.getAuthentication();
                                     try {
                                         // authentication
    -                                    CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this);
    +                                    CliAuthenticator authenticator = Jenkins.get().getSecurityRealm().createCliAuthenticator(this);
                                         new ClassParser().parse(authenticator, parser);
     
                                         // fill up all the binders
    @@ -217,7 +218,7 @@ public class CLIRegisterer extends ExtensionFinder {
                                         if (auth == Jenkins.ANONYMOUS)
                                             auth = loadStoredAuthentication();
                                         sc.setAuthentication(auth); // run the CLI with the right credential
    -                                    hudson.checkPermission(Jenkins.READ);
    +                                    jenkins.checkPermission(Jenkins.READ);
     
                                         // resolve them
                                         Object instance = null;
    diff --git a/core/src/main/java/hudson/cli/declarative/CLIResolver.java b/core/src/main/java/hudson/cli/declarative/CLIResolver.java
    index 2b31b47f2790f61d9c4b93ec142a0e6611b53967..5c4b93444e8e27f5ece3869135317d5b97a3616c 100644
    --- a/core/src/main/java/hudson/cli/declarative/CLIResolver.java
    +++ b/core/src/main/java/hudson/cli/declarative/CLIResolver.java
    @@ -40,12 +40,12 @@ import java.lang.annotation.Target;
      * <p>
      * Hudson uses the return type of the resolver method
      * to pick the resolver method to use, of all the resolver methods it discovers. That is,
    - * if Hudson is looking to find an instance of type <tt>T</tt> for the current command, it first
    - * looks for the resolver method whose return type is <tt>T</tt>, then it checks for the base type of <tt>T</tt>,
    + * if Hudson is looking to find an instance of type {@code T} for the current command, it first
    + * looks for the resolver method whose return type is {@code T}, then it checks for the base type of {@code T},
      * and so on.
      *
      * <p>
    - * If the chosen resolver method is an instance method on type <tt>S</tt>, the "parent resolver" is then
    + * If the chosen resolver method is an instance method on type {@code S}, the "parent resolver" is then
      * located to resolve an instance of type 'S'. This process repeats until a static resolver method is discovered
      * (since most of Hudson's model objects are anchored to the root {@link jenkins.model.Jenkins} object, normally that would become
      * the top-most resolver method.)
    diff --git a/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java
    index 6cd1d86eba57141695824a9ec52c22a8cad074f7..5bd9834b485b625ffd089f50f6ff9dee64105c65 100644
    --- a/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java
    +++ b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java
    @@ -57,12 +57,12 @@ public abstract class GenericItemOptionHandler<T extends Item> extends OptionHan
         protected abstract Class<T> type();
     
         @Override public int parseArguments(Parameters params) throws CmdLineException {
    -        final Jenkins j = Jenkins.getInstance();
    +        final Jenkins j = Jenkins.get();
             final String src = params.getParameter(0);
             T s = j.getItemByFullName(src, type());
             if (s == null) {
                 final Authentication who = Jenkins.getAuthentication();
    -            try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
    +            try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
                     Item actual = j.getItemByFullName(src);
                     if (actual == null) {
                         LOGGER.log(Level.FINE, "really no item exists named {0}", src);
    diff --git a/core/src/main/java/hudson/cli/handlers/NodeOptionHandler.java b/core/src/main/java/hudson/cli/handlers/NodeOptionHandler.java
    index 90905d5348968191b2d75e61ceeb01b3ca670b5f..a90199e8c1185630b152e35ddc5153dc1cd19d42 100644
    --- a/core/src/main/java/hudson/cli/handlers/NodeOptionHandler.java
    +++ b/core/src/main/java/hudson/cli/handlers/NodeOptionHandler.java
    @@ -53,7 +53,7 @@ public class NodeOptionHandler extends OptionHandler<Node> {
     
             String nodeName = params.getParameter(0);
     
    -        final Node node = Jenkins.getInstance().getNode(nodeName);
    +        final Node node = Jenkins.get().getNode(nodeName);
             if (node == null) throw new IllegalArgumentException("No such node '" + nodeName + "'");
     
             setter.addValue(node);
    diff --git a/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java b/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java
    index 729d6f571b2ed1cca2fd7707c14993577458764e..6107855812c3de7b8b16c35dd1fd8f6b1c836b2f 100644
    --- a/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java
    +++ b/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java
    @@ -48,7 +48,7 @@ import javax.annotation.CheckForNull;
      * For example:
      * <dl>
      *   <dt>my_view_name</dt><dd>refers to a top level view with given name.</dd>
    - *   <dt>nested/inner</dt><dd>refers to a view named <tt>inner</tt> inside of a top level view group named <tt>nested</tt>.</dd>
    + *   <dt>nested/inner</dt><dd>refers to a view named {@code inner} inside of a top level view group named {@code nested}.</dd>
      * </dl>
      *
      * <p>
    @@ -103,12 +103,13 @@ public class ViewOptionHandler extends OptionHandler<View> {
                 String viewName = tok.nextToken();
     
                 view = group.getView(viewName);
    -            if (view == null)
    +            if (view == null) {
    +                group.checkPermission(View.READ);
                     throw new IllegalArgumentException(String.format(
                             "No view named %s inside view %s",
                             viewName, group.getDisplayName()
                     ));
    -
    +            }
                 view.checkPermission(View.READ);
                 if (view instanceof ViewGroup) {
                     group = (ViewGroup) view;
    diff --git a/core/src/main/java/hudson/console/AnnotatedLargeText.java b/core/src/main/java/hudson/console/AnnotatedLargeText.java
    index 4e8c40658af523124c83815290ea0efaa8d2f43d..6acbdeff6e78e90b0ccbd0529e9bcd19e2b8bbec 100644
    --- a/core/src/main/java/hudson/console/AnnotatedLargeText.java
    +++ b/core/src/main/java/hudson/console/AnnotatedLargeText.java
    @@ -28,7 +28,7 @@ package hudson.console;
     import com.trilead.ssh2.crypto.Base64;
     import jenkins.model.Jenkins;
     import hudson.remoting.ObjectInputStreamEx;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import jenkins.security.CryptoConfidentialKey;
     import org.apache.commons.io.output.ByteArrayOutputStream;
     import org.kohsuke.stapler.Stapler;
    @@ -52,6 +52,7 @@ import com.jcraft.jzlib.GZIPInputStream;
     import com.jcraft.jzlib.GZIPOutputStream;
     
     import static java.lang.Math.abs;
    +import org.jenkinsci.remoting.util.AnonymousClassWarnings;
     
     /**
      * Extension to {@link LargeText} that handles annotations by {@link ConsoleAnnotator}.
    @@ -112,7 +113,7 @@ public class AnnotatedLargeText<T> extends LargeText {
             rsp.setContentType(isHtml() ? "text/html;charset=UTF-8" : "text/plain;charset=UTF-8");
         }
     
    -    private ConsoleAnnotator createAnnotator(StaplerRequest req) throws IOException {
    +    private ConsoleAnnotator<T> createAnnotator(StaplerRequest req) throws IOException {
             try {
                 String base64 = req!=null ? req.getHeader("X-ConsoleAnnotator") : null;
                 if (base64!=null) {
    @@ -123,7 +124,7 @@ public class AnnotatedLargeText<T> extends LargeText {
                             Jenkins.getInstance().pluginManager.uberClassLoader);
                     try {
                         long timestamp = ois.readLong();
    -                    if (TimeUnit2.HOURS.toMillis(1) > abs(System.currentTimeMillis()-timestamp))
    +                    if (TimeUnit.HOURS.toMillis(1) > abs(System.currentTimeMillis()-timestamp))
                             // don't deserialize something too old to prevent a replay attack
                             return (ConsoleAnnotator)ois.readObject();
                     } finally {
    @@ -134,7 +135,7 @@ public class AnnotatedLargeText<T> extends LargeText {
                 throw new IOException(e);
             }
             // start from scratch
    -        return ConsoleAnnotator.initial(context==null ? null : context.getClass());
    +        return ConsoleAnnotator.initial(context);
         }
     
         @Override
    @@ -163,13 +164,13 @@ public class AnnotatedLargeText<T> extends LargeText {
         }
     
         public long writeHtmlTo(long start, Writer w) throws IOException {
    -        ConsoleAnnotationOutputStream caw = new ConsoleAnnotationOutputStream(
    +        ConsoleAnnotationOutputStream<T> caw = new ConsoleAnnotationOutputStream<>(
                     w, createAnnotator(Stapler.getCurrentRequest()), context, charset);
             long r = super.writeLogTo(start,caw);
     
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Cipher sym = PASSING_ANNOTATOR.encrypt();
    -        ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos,sym)));
    +        ObjectOutputStream oos = AnonymousClassWarnings.checkingObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos,sym)));
             oos.writeLong(System.currentTimeMillis()); // send timestamp to prevent a replay attack
             oos.writeObject(caw.getConsoleAnnotator());
             oos.close();
    diff --git a/core/src/main/java/hudson/console/ConsoleAnnotationDescriptor.java b/core/src/main/java/hudson/console/ConsoleAnnotationDescriptor.java
    index d5ac5081364fe5f7220d2435f65940aa2b436c2e..4a74ae8a9e8aea23b0d61bd8de1e0bfeb4c23384 100644
    --- a/core/src/main/java/hudson/console/ConsoleAnnotationDescriptor.java
    +++ b/core/src/main/java/hudson/console/ConsoleAnnotationDescriptor.java
    @@ -27,7 +27,7 @@ import hudson.DescriptorExtensionList;
     import hudson.ExtensionPoint;
     import hudson.model.Descriptor;
     import jenkins.model.Jenkins;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.WebMethod;
    @@ -80,12 +80,12 @@ public abstract class ConsoleAnnotationDescriptor extends Descriptor<ConsoleNote
     
         @WebMethod(name="script.js")
         public void doScriptJs(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    -        rsp.serveFile(req, hasResource("/script.js"), TimeUnit2.DAYS.toMillis(1));
    +        rsp.serveFile(req, hasResource("/script.js"), TimeUnit.DAYS.toMillis(1));
         }
     
         @WebMethod(name="style.css")
         public void doStyleCss(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    -        rsp.serveFile(req, hasResource("/style.css"), TimeUnit2.DAYS.toMillis(1));
    +        rsp.serveFile(req, hasResource("/style.css"), TimeUnit.DAYS.toMillis(1));
         }
     
         /**
    diff --git a/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java b/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java
    index 919e3af0117fb48e9a9788e85dc2ce07d0fbcf36..f3a22c875cbdc9041424a7ab71070c9e5d8c5472 100644
    --- a/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java
    +++ b/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java
    @@ -72,7 +72,7 @@ public class ConsoleAnnotationOutputStream<T> extends LineTransformationOutputSt
             this.lineOut = new WriterOutputStream(line,charset);
         }
     
    -    public ConsoleAnnotator getConsoleAnnotator() {
    +    public ConsoleAnnotator<T> getConsoleAnnotator() {
             return ann;
         }
     
    @@ -80,6 +80,8 @@ public class ConsoleAnnotationOutputStream<T> extends LineTransformationOutputSt
          * Called after we read the whole line of plain text, which is stored in {@link #buf}.
          * This method performs annotations and send the result to {@link #out}.
          */
    +    @SuppressWarnings({"unchecked", "rawtypes"}) // appears to be unsound
    +    @Override
         protected void eol(byte[] in, int sz) throws IOException {
             line.reset();
             final StringBuffer strBuf = line.getStringBuffer();
    @@ -109,9 +111,10 @@ public class ConsoleAnnotationOutputStream<T> extends LineTransformationOutputSt
                         final ConsoleNote a = ConsoleNote.readFrom(new DataInputStream(b));
                         if (a!=null) {
                             if (annotators==null)
    -                            annotators = new ArrayList<ConsoleAnnotator<T>>();
    +                            annotators = new ArrayList<>();
                             annotators.add(new ConsoleAnnotator<T>() {
    -                            public ConsoleAnnotator annotate(T context, MarkupText text) {
    +                            @Override
    +                            public ConsoleAnnotator<T> annotate(T context, MarkupText text) {
                                     return a.annotate(context,text,charPos);
                                 }
                             });
    diff --git a/core/src/main/java/hudson/console/ConsoleAnnotator.java b/core/src/main/java/hudson/console/ConsoleAnnotator.java
    index c19ac974b686064cd0adb7767065c7d04a6a3af4..9701eb302f04d54476154473e0b8bc85aa5fe346 100644
    --- a/core/src/main/java/hudson/console/ConsoleAnnotator.java
    +++ b/core/src/main/java/hudson/console/ConsoleAnnotator.java
    @@ -82,50 +82,54 @@ public abstract class ConsoleAnnotator<T> implements Serializable {
          *      To indicate that you are not interested in the following lines, return {@code null}.
          */
         @CheckForNull
    -    public abstract ConsoleAnnotator annotate(@Nonnull T context, @Nonnull MarkupText text );
    +    public abstract ConsoleAnnotator<T> annotate(@Nonnull T context, @Nonnull MarkupText text );
     
         /**
          * Cast operation that restricts T.
          */
    +    @SuppressWarnings("unchecked")
         public static <T> ConsoleAnnotator<T> cast(ConsoleAnnotator<? super T> a) {
             return (ConsoleAnnotator)a;
         }
     
    -    /**
    -     * Bundles all the given {@link ConsoleAnnotator} into a single annotator.
    -     */
    -    public static <T> ConsoleAnnotator<T> combine(Collection<? extends ConsoleAnnotator<? super T>> all) {
    -        switch (all.size()) {
    -        case 0:     return null;    // none
    -        case 1:     return  cast(all.iterator().next()); // just one
    -        }
    -
    -        class Aggregator extends ConsoleAnnotator<T> {
    -            List<ConsoleAnnotator<T>> list;
    +    @SuppressWarnings({"unchecked", "rawtypes"}) // unclear to jglick what is going on here
    +    private static final class ConsoleAnnotatorAggregator<T> extends ConsoleAnnotator<T> {
    +        List<ConsoleAnnotator<T>> list;
     
    -            Aggregator(Collection list) {
    -                this.list = new ArrayList<ConsoleAnnotator<T>>(list);
    -            }
    +        ConsoleAnnotatorAggregator(Collection list) {
    +            this.list = new ArrayList<>(list);
    +        }
     
    -            public ConsoleAnnotator annotate(T context, MarkupText text) {
    -                ListIterator<ConsoleAnnotator<T>> itr = list.listIterator();
    -                while (itr.hasNext()) {
    -                    ConsoleAnnotator a =  itr.next();
    -                    ConsoleAnnotator b = a.annotate(context,text);
    -                    if (a!=b) {
    -                        if (b==null)    itr.remove();
    -                        else            itr.set(b);
    -                    }
    +        @Override
    +        public ConsoleAnnotator annotate(T context, MarkupText text) {
    +            ListIterator<ConsoleAnnotator<T>> itr = list.listIterator();
    +            while (itr.hasNext()) {
    +                ConsoleAnnotator a =  itr.next();
    +                ConsoleAnnotator b = a.annotate(context,text);
    +                if (a!=b) {
    +                    if (b==null)    itr.remove();
    +                    else            itr.set(b);
                     }
    +            }
     
    -                switch (list.size()) {
    +            switch (list.size()) {
                     case 0:     return null;    // no more annotator left
                     case 1:     return list.get(0); // no point in aggregating
                     default:    return this;
    -                }
                 }
             }
    -        return new Aggregator(all);
    +    }
    +
    +    /**
    +     * Bundles all the given {@link ConsoleAnnotator} into a single annotator.
    +     */
    +    public static <T> ConsoleAnnotator<T> combine(Collection<? extends ConsoleAnnotator<? super T>> all) {
    +        switch (all.size()) {
    +        case 0:     return null;    // none
    +        case 1:     return  cast(all.iterator().next()); // just one
    +        }
    +
    +        return new ConsoleAnnotatorAggregator<>(all);
         }
     
         /**
    @@ -139,8 +143,9 @@ public abstract class ConsoleAnnotator<T> implements Serializable {
         /**
          * List all the console annotators that can work for the specified context type.
          */
    +    @SuppressWarnings({"unchecked", "rawtypes"}) // reflective
         public static <T> List<ConsoleAnnotator<T>> _for(T context) {
    -        List<ConsoleAnnotator<T>> r  = new ArrayList<ConsoleAnnotator<T>>();
    +        List<ConsoleAnnotator<T>> r  = new ArrayList<>();
             for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) {
                 if (f.type().isInstance(context)) {
                     ConsoleAnnotator ca = f.newInstance(context);
    diff --git a/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java b/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java
    index 9ae2a74df492f1388f925a16323074eca53f4858..409ee59c67f4c531996b33b20e9a6f33bb6b14f0 100644
    --- a/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java
    +++ b/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java
    @@ -27,7 +27,7 @@ import hudson.Extension;
     import hudson.ExtensionList;
     import hudson.ExtensionPoint;
     import hudson.model.Run;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import org.jvnet.tiger_types.Types;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
    @@ -58,7 +58,7 @@ import java.net.URL;
      *
      * <h2>Behaviour, JavaScript, and CSS</h2>
      * <p>
    - * {@link ConsoleNote} can have associated <tt>script.js</tt> and <tt>style.css</tt> (put them
    + * {@link ConsoleNote} can have associated {@code script.js} and {@code style.css} (put them
      * in the same resource directory that you normally put Jelly scripts), which will be loaded into
      * the HTML page whenever the console notes are used. This allows you to use minimal markup in
      * code generation, and do the styling in CSS and perform the rest of the interesting work as a CSS behaviour/JavaScript.
    @@ -80,13 +80,13 @@ public abstract class ConsoleAnnotatorFactory<T> implements ExtensionPoint {
          * @return
          *      null if this factory is not going to participate in the annotation of this console.
          */
    -    public abstract ConsoleAnnotator newInstance(T context);
    +    public abstract ConsoleAnnotator<T> newInstance(T context);
     
         /**
          * For which context type does this annotator work?
          */
    -    public Class type() {
    -        Type type = Types.getBaseClass(getClass(), ConsoleAnnotator.class);
    +    public Class<?> type() {
    +        Type type = Types.getBaseClass(getClass(), ConsoleAnnotatorFactory.class);
             if (type instanceof ParameterizedType)
                 return Types.erasure(Types.getTypeArgument(type,0));
             else
    @@ -105,7 +105,7 @@ public abstract class ConsoleAnnotatorFactory<T> implements ExtensionPoint {
         }
     
         private URL getResource(String fileName) {
    -        Class c = getClass();
    +        Class<?> c = getClass();
             return c.getClassLoader().getResource(c.getName().replace('.','/').replace('$','/')+ fileName);
         }
     
    @@ -114,17 +114,18 @@ public abstract class ConsoleAnnotatorFactory<T> implements ExtensionPoint {
          */
         @WebMethod(name="script.js")
         public void doScriptJs(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    -        rsp.serveFile(req, getResource("/script.js"), TimeUnit2.DAYS.toMillis(1));
    +        rsp.serveFile(req, getResource("/script.js"), TimeUnit.DAYS.toMillis(1));
         }
     
         @WebMethod(name="style.css")
         public void doStyleCss(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    -        rsp.serveFile(req, getResource("/style.css"), TimeUnit2.DAYS.toMillis(1));
    +        rsp.serveFile(req, getResource("/style.css"), TimeUnit.DAYS.toMillis(1));
         }
     
         /**
          * All the registered instances.
          */
    +    @SuppressWarnings("rawtypes")
         public static ExtensionList<ConsoleAnnotatorFactory> all() {
             return ExtensionList.lookup(ConsoleAnnotatorFactory.class);
         }
    diff --git a/core/src/main/java/hudson/console/ConsoleNote.java b/core/src/main/java/hudson/console/ConsoleNote.java
    index c0b281c6ca9bce6cfda181c35b98aca9ae8dbe45..b4f67cf3d6a7e6bd3df599e6481d769db022a32a 100644
    --- a/core/src/main/java/hudson/console/ConsoleNote.java
    +++ b/core/src/main/java/hudson/console/ConsoleNote.java
    @@ -53,7 +53,11 @@ import com.jcraft.jzlib.GZIPInputStream;
     import com.jcraft.jzlib.GZIPOutputStream;
     import hudson.remoting.ClassFilter;
     import jenkins.security.HMACConfidentialKey;
    +import jenkins.util.JenkinsJVM;
     import jenkins.util.SystemProperties;
    +import org.jenkinsci.remoting.util.AnonymousClassWarnings;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
      * Data that hangs off from a console output.
    @@ -108,7 +112,7 @@ import jenkins.util.SystemProperties;
      *
      * <h2>Behaviour, JavaScript, and CSS</h2>
      * <p>
    - * {@link ConsoleNote} can have associated <tt>script.js</tt> and <tt>style.css</tt> (put them
    + * {@link ConsoleNote} can have associated {@code script.js} and {@code style.css} (put them
      * in the same resource directory that you normally put Jelly scripts), which will be loaded into
      * the HTML page whenever the console notes are used. This allows you to use minimal markup in
      * code generation, and do the styling in CSS and perform the rest of the interesting work as a CSS behaviour/JavaScript.
    @@ -129,7 +133,8 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
          * Disables checking of {@link #MAC} so do not set this flag unless you completely trust all users capable of affecting build output,
          * which in practice means that all SCM committers as well as all Jenkins users with any non-read-only access are consider administrators.
          */
    -    static /* nonfinal for tests & script console */ boolean INSECURE = SystemProperties.getBoolean(ConsoleNote.class.getName() + ".INSECURE");
    +    @Restricted(NoExternalUse.class)
    +    public static /* nonfinal for tests & script console */ boolean INSECURE = SystemProperties.getBoolean(ConsoleNote.class.getName() + ".INSECURE");
     
         /**
          * When the line of a console output that this annotation is attached is read by someone,
    @@ -180,7 +185,8 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
     
         private ByteArrayOutputStream encodeToBytes() throws IOException {
             ByteArrayOutputStream buf = new ByteArrayOutputStream();
    -        try (ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(buf))) {
    +        try (OutputStream gzos = new GZIPOutputStream(buf);
    +             ObjectOutputStream oos = JenkinsJVM.isJenkinsJVM() ? AnonymousClassWarnings.checkingObjectOutputStream(gzos) : new ObjectOutputStream(gzos)) {
                 oos.writeObject(this);
             }
     
    @@ -189,7 +195,7 @@ public abstract class ConsoleNote<T> implements Serializable, Describable<Consol
             DataOutputStream dos = new DataOutputStream(new Base64OutputStream(buf2,true,-1,null));
             try {
                 buf2.write(PREAMBLE);
    -            if (Jenkins.getInstanceOrNull() != null) { // else we are in another JVM and cannot sign; result will be ignored unless INSECURE
    +            if (JenkinsJVM.isJenkinsJVM()) { // else we are in another JVM and cannot sign; result will be ignored unless INSECURE
                     byte[] mac = MAC.mac(buf.toByteArray());
                     dos.writeInt(- mac.length); // negative to differentiate from older form
                     dos.write(mac);
    diff --git a/core/src/main/java/hudson/console/HudsonExceptionNote.java b/core/src/main/java/hudson/console/HudsonExceptionNote.java
    index 83511694ceca4028e4b58011bba677d508846d23..574bdabb2274fa2371719b56c51273e7ebed55fb 100644
    --- a/core/src/main/java/hudson/console/HudsonExceptionNote.java
    +++ b/core/src/main/java/hudson/console/HudsonExceptionNote.java
    @@ -40,7 +40,7 @@ import org.jenkinsci.Symbol;
      *
      * @author Kohsuke Kawaguchi
      * @since 1.349 - produces search hyperlinks to the http://stacktrace.jenkins-ci.org service
    - * @since TODO - does nothing due to JENKINS-42861
    + * @since 2.56 - does nothing due to JENKINS-42861
      * @deprecated This ConsoleNote used to provide hyperlinks to the
      *             <code>http://stacktrace.jenkins-ci.org/</code> service, which is dead now (JENKINS-42861).
      *             This console note does nothing right now.
    diff --git a/core/src/main/java/hudson/console/HyperlinkNote.java b/core/src/main/java/hudson/console/HyperlinkNote.java
    index 79fbbffc70ec1284d530da6b7eeec284e4b195d7..3f8ccfda1d89ab00a50e809cafb4b443c31d8e76 100644
    --- a/core/src/main/java/hudson/console/HyperlinkNote.java
    +++ b/core/src/main/java/hudson/console/HyperlinkNote.java
    @@ -27,10 +27,13 @@ import hudson.Extension;
     import hudson.MarkupText;
     import jenkins.model.Jenkins;
     import org.jenkinsci.Symbol;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.Stapler;
     import org.kohsuke.stapler.StaplerRequest;
     
     import java.io.IOException;
    +import java.util.function.BiFunction;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     
    @@ -75,8 +78,20 @@ public class HyperlinkNote extends ConsoleNote {
         }
     
         public static String encodeTo(String url, String text) {
    +        return encodeTo(url, text, HyperlinkNote::new);
    +    }
    +
    +    @Restricted(NoExternalUse.class)
    +    static String encodeTo(String url, String text, BiFunction<String, Integer, ConsoleNote> constructor) {
    +        // If text contains newlines, then its stored length will not match its length when being
    +        // displayed, since the display length will only include text up to the first newline,
    +        // which will cause an IndexOutOfBoundsException in MarkupText#rangeCheck when
    +        // ConsoleAnnotationOutputStream converts the note into markup. That stream treats '\n' as
    +        // the sole end-of-line marker on all platforms, so we ignore '\r' because it will not
    +        // break the conversion.
    +        text = text.replace('\n', ' ');
             try {
    -            return new HyperlinkNote(url,text.length()).encode()+text;
    +            return constructor.apply(url,text.length()).encode()+text;
             } catch (IOException e) {
                 // impossible, but don't make this a fatal problem
                 LOGGER.log(Level.WARNING, "Failed to serialize "+HyperlinkNote.class,e);
    @@ -92,4 +107,5 @@ public class HyperlinkNote extends ConsoleNote {
         }
     
         private static final Logger LOGGER = Logger.getLogger(HyperlinkNote.class.getName());
    +    private static final long serialVersionUID = 3908468829358026949L;
     }
    diff --git a/core/src/main/java/hudson/console/ModelHyperlinkNote.java b/core/src/main/java/hudson/console/ModelHyperlinkNote.java
    index 784934708d62dc95389f0877fd571601d865e61c..127911cc76fa848ac5dee822634739d815960c65 100644
    --- a/core/src/main/java/hudson/console/ModelHyperlinkNote.java
    +++ b/core/src/main/java/hudson/console/ModelHyperlinkNote.java
    @@ -5,8 +5,6 @@ import hudson.model.*;
     import jenkins.model.Jenkins;
     import org.jenkinsci.Symbol;
     
    -import java.io.IOException;
    -import java.util.logging.Level;
     import java.util.logging.Logger;
     import javax.annotation.Nonnull;
     
    @@ -57,13 +55,7 @@ public class ModelHyperlinkNote extends HyperlinkNote {
         }
     
         public static String encodeTo(String url, String text) {
    -        try {
    -            return new ModelHyperlinkNote(url,text.length()).encode()+text;
    -        } catch (IOException e) {
    -            // impossible, but don't make this a fatal problem
    -            LOGGER.log(Level.WARNING, "Failed to serialize "+ModelHyperlinkNote.class,e);
    -            return text;
    -        }
    +        return HyperlinkNote.encodeTo(url, text, ModelHyperlinkNote::new);
         }
     
         @Extension @Symbol("hyperlinkToModels")
    diff --git a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java
    index 3d842e1d79c5b9bf9535339a4952abd5876c3ee2..56eaf7d941a0f281dd45a3f219204fce04719353 100644
    --- a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java
    +++ b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java
    @@ -31,7 +31,7 @@ import org.jenkinsci.Symbol;
     import java.util.logging.Logger;
     
     /**
    - * Periodically checks the disk usage of <tt>JENKINS_HOME</tt>,
    + * Periodically checks the disk usage of {@code JENKINS_HOME},
      * and activate {@link HudsonHomeDiskUsageMonitor} if necessary.
      *
      * @author Kohsuke Kawaguchi
    diff --git a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java
    index 337f739dcb827d0d718d83208ff47cc909bf6b00..177141f746fa872774cc63aa1adea6d8fd52cd1b 100644
    --- a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java
    +++ b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java
    @@ -38,7 +38,7 @@ import java.io.IOException;
     import java.util.List;
     
     /**
    - * Monitors the disk usage of <tt>JENKINS_HOME</tt>, and if it's almost filled up, warn the user.
    + * Monitors the disk usage of {@code JENKINS_HOME}, and if it's almost filled up, warn the user.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/diagnosis/MemoryUsageMonitor.java b/core/src/main/java/hudson/diagnosis/MemoryUsageMonitor.java
    index 23ad5140b8280a5c3388916e806a770efffe4d23..1ba648eb38fe142462d98cea457473130c77e4eb 100644
    --- a/core/src/main/java/hudson/diagnosis/MemoryUsageMonitor.java
    +++ b/core/src/main/java/hudson/diagnosis/MemoryUsageMonitor.java
    @@ -23,7 +23,7 @@
      */
     package hudson.diagnosis;
     
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import hudson.util.ColorPalette;
     import hudson.Extension;
     import hudson.model.PeriodicWork;
    @@ -116,7 +116,7 @@ public final class MemoryUsageMonitor extends PeriodicWork {
         }
     
         public long getRecurrencePeriod() {
    -        return TimeUnit2.SECONDS.toMillis(10);
    +        return TimeUnit.SECONDS.toMillis(10);
         }
     
         protected void doRun() {
    diff --git a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java
    index 6bbeed6cf5b7c6dfd55d97c8a617443e2ab2b966..7fc185737c1563f8fb109ce4525fd1ee6d66cc0e 100644
    --- a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java
    +++ b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java
    @@ -26,6 +26,7 @@ package hudson.diagnosis;
     import com.google.common.base.Predicate;
     import com.thoughtworks.xstream.converters.UnmarshallingContext;
     import hudson.Extension;
    +import hudson.ExtensionList;
     import hudson.XmlFile;
     import hudson.model.AdministrativeMonitor;
     import hudson.model.Item;
    @@ -52,6 +53,7 @@ import java.util.concurrent.ConcurrentMap;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     
     import jenkins.model.Jenkins;
     import org.acegisecurity.context.SecurityContext;
    @@ -78,8 +80,16 @@ public class OldDataMonitor extends AdministrativeMonitor {
     
         private ConcurrentMap<SaveableReference,VersionRange> data = new ConcurrentHashMap<SaveableReference,VersionRange>();
     
    -    static OldDataMonitor get(Jenkins j) {
    -        return (OldDataMonitor) j.getAdministrativeMonitor("OldData");
    +    /**
    +     * Gets instance of the monitor.
    +     * @param j Jenkins instance
    +     * @return Monitor instance
    +     * @throws IllegalStateException Monitor not found.
    +     *              It should never happen since the monitor is located in the core.
    +     */
    +    @Nonnull
    +    static OldDataMonitor get(Jenkins j) throws IllegalStateException {
    +        return ExtensionList.lookupSingleton(OldDataMonitor.class);
         }
     
         public OldDataMonitor() {
    diff --git a/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java b/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java
    index 38a9215c22aebd50389331a6ac5a6b6685f6b5fe..858f79ddaad89b84974b90d1113f5b0e88b269c7 100644
    --- a/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java
    +++ b/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java
    @@ -26,6 +26,7 @@ package hudson.diagnosis;
     import hudson.Extension;
     import hudson.Util;
     import hudson.model.AdministrativeMonitor;
    +import jenkins.security.stapler.StaplerDispatchable;
     import org.jenkinsci.Symbol;
     import org.kohsuke.stapler.HttpRedirect;
     import org.kohsuke.stapler.HttpResponse;
    @@ -70,6 +71,7 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor {
             return new HttpRedirect(redirect);
         }
     
    +    @StaplerDispatchable
         public void getTestForReverseProxySetup(String rest) {
             Jenkins j = Jenkins.getInstance();
             String inferred = j.getRootUrlFromRequest() + "manage";
    @@ -92,7 +94,7 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor {
                 // of course the irony is that this redirect won't work
                 return HttpResponses.redirectViaContextPath("/manage");
             } else {
    -            return new HttpRedirect("https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+says+my+reverse+proxy+setup+is+broken");
    +            return new HttpRedirect("https://jenkins.io/redirect/troubleshooting/broken-reverse-proxy");
             }
         }
     
    diff --git a/core/src/main/java/hudson/init/InitMilestone.java b/core/src/main/java/hudson/init/InitMilestone.java
    index 3d5412e0bffca0ffc4c4b44d7c4d1d1f895320e7..b6bd3a528fdf1b19a12eb9fada34302a7a339fc8 100644
    --- a/core/src/main/java/hudson/init/InitMilestone.java
    +++ b/core/src/main/java/hudson/init/InitMilestone.java
    @@ -82,7 +82,7 @@ public enum InitMilestone implements Milestone {
          * By this milestone, all programmatically constructed extension point implementations
          * should be added.
          */
    -    EXTENSIONS_AUGMENTED("Augmented all extensions"),
    +    EXTENSIONS_AUGMENTED("Augmented all extensions"), // TODO nothing attains() this so when does it actually happen?
     
         /**
          * By this milestone, all jobs and their build records are loaded from disk.
    diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java
    index 0d1e037e6a6bc5cf939ea2da9bc42d5f058e5147..9d7d287c4310237183984b3287f7536867d76cde 100644
    --- a/core/src/main/java/hudson/init/InitStrategy.java
    +++ b/core/src/main/java/hudson/init/InitStrategy.java
    @@ -17,7 +17,8 @@ import hudson.PluginManager;
     import jenkins.util.SystemProperties;
     import hudson.util.DirScanner;
     import hudson.util.FileVisitor;
    -import hudson.util.Service;
    +import java.util.Iterator;
    +import java.util.ServiceLoader;
     
     /**
      * Strategy pattern of the various key decision making during the Jenkins initialization.
    @@ -113,11 +114,12 @@ public class InitStrategy {
          * Obtains the instance to be used.
          */
         public static InitStrategy get(ClassLoader cl) throws IOException {
    -        List<InitStrategy> r = Service.loadInstances(cl, InitStrategy.class);
    -        if (r.isEmpty())    return new InitStrategy();      // default
    -
    -        InitStrategy s = r.get(0);
    -        LOGGER.fine("Using "+s+" as InitStrategy");
    +        Iterator<InitStrategy> it = ServiceLoader.load(InitStrategy.class, cl).iterator();
    +        if (!it.hasNext()) {
    +            return new InitStrategy(); // default
    +        }
    +        InitStrategy s = it.next();
    +        LOGGER.log(Level.FINE, "Using {0} as InitStrategy", s);
             return s;
         }
     
    diff --git a/core/src/main/java/hudson/init/Initializer.java b/core/src/main/java/hudson/init/Initializer.java
    index d41f45da52edba6cc1637d7c33f0bfb4eee50c29..d24a412ae8a30e5d6dacd61f56924e6646aeabd2 100644
    --- a/core/src/main/java/hudson/init/Initializer.java
    +++ b/core/src/main/java/hudson/init/Initializer.java
    @@ -92,7 +92,7 @@ public @interface Initializer {
         String[] attains() default {};
     
         /**
    -     * Key in <tt>Messages.properties</tt> that represents what this task is about. Used for rendering the progress.
    +     * Key in {@code Messages.properties} that represents what this task is about. Used for rendering the progress.
          * Defaults to "${short class name}.${method Name}".
          */
         String displayName() default "";
    diff --git a/core/src/main/java/hudson/init/Terminator.java b/core/src/main/java/hudson/init/Terminator.java
    index 3e1155babdcfee7954b507a33d93d81fd4a24be9..24bc781fb10a1d5ef8f2a037a8c5b2ef9eb51202 100644
    --- a/core/src/main/java/hudson/init/Terminator.java
    +++ b/core/src/main/java/hudson/init/Terminator.java
    @@ -52,7 +52,7 @@ public @interface Terminator {
         String[] attains() default {};
     
         /**
    -     * Key in <tt>Messages.properties</tt> that represents what this task is about. Used for rendering the progress.
    +     * Key in {@code Messages.properties} that represents what this task is about. Used for rendering the progress.
          * Defaults to "${short class name}.${method Name}".
          */
         String displayName() default "";
    diff --git a/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java b/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java
    index d8412f6ad45c3c0a8fda8c3c1fd3e00da87cc1f4..a092a5123a2b206a89398c9e61ec4e55f451ad85 100644
    --- a/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java
    +++ b/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java
    @@ -1,6 +1,7 @@
     package hudson.init.impl;
     
     import hudson.init.Initializer;
    +import java.io.EOFException;
     import jenkins.model.Jenkins;
     import org.kohsuke.stapler.WebApp;
     import org.kohsuke.stapler.compression.CompressionFilter;
    @@ -17,7 +18,7 @@ import java.util.logging.Logger;
     import org.kohsuke.stapler.Stapler;
     
     /**
    - * @author Kohsuke Kawaguchi
    + * Deals with exceptions that get thrown all the way up to the Stapler rendering layer.
      */
     public class InstallUncaughtExceptionHandler {
     
    @@ -25,23 +26,19 @@ public class InstallUncaughtExceptionHandler {
     
         @Initializer
         public static void init(final Jenkins j) throws IOException {
    -        CompressionFilter.setUncaughtExceptionHandler(j.servletContext, new UncaughtExceptionHandler() {
    -            @Override
    -            public void reportException(Throwable e, ServletContext context, HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException {
    +        CompressionFilter.setUncaughtExceptionHandler(j.servletContext, (e, context, req, rsp) -> {
                     if (rsp.isCommitted()) {
    -                    LOGGER.log(Level.WARNING, null, e);
    +                    LOGGER.log(isEOFException(e) ? Level.FINE : Level.WARNING, null, e);
                         return;
                     }
                     req.setAttribute("javax.servlet.error.exception",e);
                     try {
    -                    WebApp.get(j.servletContext).getSomeStapler()
    -                            .invoke(req,rsp, Jenkins.getInstance(), "/oops");
    +                    WebApp.get(j.servletContext).getSomeStapler().invoke(req, rsp, j, "/oops");
                     } catch (ServletException | IOException x) {
                         if (!Stapler.isSocketException(x)) {
                             throw x;
                         }
                     }
    -            }
             });
             try {
                 Thread.setDefaultUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler());
    @@ -57,6 +54,16 @@ public class InstallUncaughtExceptionHandler {
             }
         }
     
    +    private static boolean isEOFException(Throwable e) {
    +        if (e == null) {
    +            return false;
    +        } else if (e instanceof EOFException) {
    +            return true;
    +        } else {
    +            return isEOFException(e.getCause());
    +        }
    +    }
    +
         /** An UncaughtExceptionHandler that just logs the exception */
         private static class DefaultUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
     
    diff --git a/core/src/main/java/hudson/init/package-info.java b/core/src/main/java/hudson/init/package-info.java
    index 9833d2090ec5adc3f1444d1a8828a2a9f1f519c9..5f66063ca3b2ef245b9491cdf4488513d8421b9d 100644
    --- a/core/src/main/java/hudson/init/package-info.java
    +++ b/core/src/main/java/hudson/init/package-info.java
    @@ -33,7 +33,7 @@
      *
      * <p>
      * Such micro-scopic dependencies are organized into a bigger directed acyclic graph, which is then executed
    - * via <tt>Session</tt>. During execution of the reactor, additional tasks can be discovered and added to
    + * via {@code Session}. During execution of the reactor, additional tasks can be discovered and added to
      * the DAG. We use this additional indirection to:
      *
      * <ol>
    diff --git a/core/src/main/java/hudson/lifecycle/ExitLifecycle.java b/core/src/main/java/hudson/lifecycle/ExitLifecycle.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..4d15a575a313cf9ce7e7a0301038184218e916da
    --- /dev/null
    +++ b/core/src/main/java/hudson/lifecycle/ExitLifecycle.java
    @@ -0,0 +1,75 @@
    +/*
    + * The MIT License
    + *
    + * Copyright 2018 Alon Bar-Lev <alon.barlev@gmail.com>
    + *
    + * 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.lifecycle;
    +
    +import hudson.Extension;
    +
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +
    +import jenkins.model.Configuration;
    +import jenkins.model.Jenkins;
    +
    +/**
    + * {@link Lifecycle} that delegates the responsibility to restart Jenkins to an external
    + * watchdog such as SystemD or OpenRC.
    + *
    + * <p>
    + * Restart by exit with specific code.
    + *
    + * @author Alon Bar-Lev
    + */
    +@Restricted(NoExternalUse.class)
    +@Extension
    +public class ExitLifecycle extends Lifecycle {
    +
    +    private static final Logger LOGGER = Logger.getLogger(ExitLifecycle.class.getName());
    +
    +    private static final String EXIT_CODE_ON_RESTART = "exitCodeOnRestart";
    +    private static final String DEFAULT_EXIT_CODE = "5";
    +
    +    private Integer exitOnRestart;
    +
    +    public ExitLifecycle() {
    +        exitOnRestart = Integer.parseInt(Configuration.getStringConfigParameter(EXIT_CODE_ON_RESTART, DEFAULT_EXIT_CODE));
    +    }
    +
    +    @Override
    +    public void restart() {
    +        Jenkins jenkins = Jenkins.getInstanceOrNull(); // guard against repeated concurrent calls to restart
    +
    +        try {
    +            if (jenkins != null) {
    +                jenkins.cleanUp();
    +            }
    +        } catch (Exception e) {
    +            LOGGER.log(Level.SEVERE, "Failed to clean up. Restart will continue.", e);
    +        }
    +
    +        System.exit(exitOnRestart);
    +    }
    +}
    diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java
    index 34915da0de1beab1dbd7c085506d8bb51aab2644..4c52cdd87877e8e91f588831cdfbb244dba9c2d4 100644
    --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java
    +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java
    @@ -112,7 +112,7 @@ public abstract class Lifecycle implements ExtensionPoint {
         }
     
         /**
    -     * If the location of <tt>jenkins.war</tt> is known in this life cycle,
    +     * If the location of {@code jenkins.war} is known in this life cycle,
          * return it location. Otherwise return null to indicate that it is unknown.
          *
          * <p>
    @@ -131,7 +131,7 @@ public abstract class Lifecycle implements ExtensionPoint {
          *
          * <p>
          * On some system, most notably Windows, a file being in use cannot be changed,
    -     * so rewriting <tt>jenkins.war</tt> requires some special trick. Override this method
    +     * so rewriting {@code jenkins.war} requires some special trick. Override this method
          * to do so.
          */
         public void rewriteHudsonWar(File by) throws IOException {
    diff --git a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java
    index 14bbb60584bca38a207ca23e8d52c5c8f4a72874..b218d5964077bb45df6909a644e0892276561b18 100644
    --- a/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java
    +++ b/core/src/main/java/hudson/lifecycle/WindowsInstallerLink.java
    @@ -112,6 +112,8 @@ public class WindowsInstallerLink extends ManagementLink {
          */
         @RequirePOST
         public void doDoInstall(StaplerRequest req, StaplerResponse rsp, @QueryParameter("dir") String _dir) throws IOException, ServletException {
    +        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +
             if(installationDir!=null) {
                 // installation already complete
                 sendError("Installation is already complete",req,rsp);
    @@ -121,8 +123,6 @@ public class WindowsInstallerLink extends ManagementLink {
                 sendError(".NET Framework 2.0 or later is required for this feature",req,rsp);
                 return;
             }
    -        
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
     
             File dir = new File(_dir).getAbsoluteFile();
             dir.mkdirs();
    @@ -174,6 +174,8 @@ public class WindowsInstallerLink extends ManagementLink {
     
         @RequirePOST
         public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    +        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +
             if(installationDir==null) {
                 // if the user reloads the page after Hudson has restarted,
                 // it comes back here. In such a case, don't let this restart Hudson.
    @@ -181,7 +183,6 @@ public class WindowsInstallerLink extends ManagementLink {
                 rsp.sendRedirect(req.getContextPath()+"/");
                 return;
             }
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
     
             rsp.forward(this,"_restart",req);
             final File oldRoot = Jenkins.getInstance().getRootDir();
    diff --git a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java
    index 94066417b0baf978edc37ae877a231869ffcd791..ad3940b4a4feab58375654f6a0f0bb870c18ad1b 100644
    --- a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java
    +++ b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java
    @@ -54,26 +54,26 @@ public class WindowsServiceLifecycle extends Lifecycle {
         }
     
         /**
    -     * If <tt>jenkins.exe</tt> is old compared to our copy,
    +     * If {@code jenkins.exe} is old compared to our copy,
          * schedule an overwrite (except that since it's currently running,
          * we can only do it when Jenkins restarts next time.)
          */
         private void updateJenkinsExeIfNeeded() {
             try {
    -            File rootDir = Jenkins.getInstance().getRootDir();
    +            File baseDir = getBaseDir();
     
                 URL exe = getClass().getResource("/windows-service/jenkins.exe");
                 String ourCopy = Util.getDigestOf(exe.openStream());
     
                 for (String name : new String[]{"hudson.exe","jenkins.exe"}) {
                     try {
    -                    File currentCopy = new File(rootDir,name);
    +                    File currentCopy = new File(baseDir,name);
                         if(!currentCopy.exists())   continue;
                         String curCopy = new FilePath(currentCopy).digest();
     
                         if(ourCopy.equals(curCopy))     continue; // identical
     
    -                    File stage = new File(rootDir,name+".new");
    +                    File stage = new File(baseDir,name+".new");
                         FileUtils.copyURLToFile(exe,stage);
                         Kernel32.INSTANCE.MoveFileExA(stage.getAbsolutePath(),currentCopy.getAbsolutePath(),MOVEFILE_DELAY_UNTIL_REBOOT|MOVEFILE_REPLACE_EXISTING);
                         LOGGER.info("Scheduled a replacement of "+name);
    @@ -107,8 +107,8 @@ public class WindowsServiceLifecycle extends Lifecycle {
             String baseName = dest.getName();
             baseName = baseName.substring(0,baseName.indexOf('.'));
     
    -        File rootDir = Jenkins.getInstance().getRootDir();
    -        File copyFiles = new File(rootDir,baseName+".copies");
    +        File baseDir = getBaseDir();
    +        File copyFiles = new File(baseDir,baseName+".copies");
     
             try (FileWriter w = new FileWriter(copyFiles, true)) {
                 w.write(by.getAbsolutePath() + '>' + getHudsonWar().getAbsolutePath() + '\n');
    @@ -144,6 +144,19 @@ public class WindowsServiceLifecycle extends Lifecycle {
             if(r!=0)
                 throw new IOException(baos.toString());
         }
    +    
    +    private static final File getBaseDir() {
    +        File baseDir;
    +        
    +        String baseEnv = System.getenv("BASE");
    +        if (baseEnv != null) {
    +            baseDir = new File(baseEnv);
    +        } else {
    +            LOGGER.log(Level.WARNING, "Could not find environment variable 'BASE' for Jenkins base directory. Falling back to JENKINS_HOME");
    +            baseDir = Jenkins.getInstance().getRootDir();
    +        }
    +        return baseDir;
    +    }
     
         private static final Logger LOGGER = Logger.getLogger(WindowsServiceLifecycle.class.getName());
     }
    diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java
    index 7313778df04399340fc09ac394bee6a6195bb4e7..51549a3bd34d8453945b08d344ed40b2f930239b 100644
    --- a/core/src/main/java/hudson/logging/LogRecorder.java
    +++ b/core/src/main/java/hudson/logging/LogRecorder.java
    @@ -23,6 +23,7 @@
      */
     package hudson.logging;
     
    +import com.google.common.annotations.VisibleForTesting;
     import com.thoughtworks.xstream.XStream;
     import hudson.BulkChange;
     import hudson.Extension;
    @@ -31,6 +32,7 @@ import hudson.Util;
     import hudson.XmlFile;
     import hudson.model.*;
     import hudson.util.HttpResponses;
    +import jenkins.util.MemoryReductionUtil;
     import jenkins.model.Jenkins;
     import hudson.model.listeners.SaveableListener;
     import hudson.remoting.Channel;
    @@ -41,6 +43,7 @@ import hudson.util.RingBufferLogHandler;
     import hudson.util.XStream2;
     import jenkins.security.MasterToSlaveCallable;
     import net.sf.json.JSONObject;
    +import org.apache.commons.lang.StringUtils;
     import org.kohsuke.stapler.*;
     import org.kohsuke.stapler.interceptor.RequirePOST;
     
    @@ -85,16 +88,60 @@ public class LogRecorder extends AbstractModelObject implements Saveable {
             return ts;
         }
     
    +    @Restricted(NoExternalUse.class)
    +    @VisibleForTesting
    +    public static Set<String> getAutoCompletionCandidates(List<String> loggerNamesList) {
    +        Set<String> loggerNames = new HashSet<>(loggerNamesList);
    +
    +        // now look for package prefixes that make sense to offer for autocompletion:
    +        // Only prefixes that match multiple loggers will be shown.
    +        // Example: 'org' will show 'org', because there's org.apache, org.jenkinsci, etc.
    +        // 'io' might only show 'io.jenkins.plugins' rather than 'io' if all loggers starting with 'io' start with 'io.jenkins.plugins'.
    +        HashMap<String, Integer> seenPrefixes = new HashMap<>();
    +        SortedSet<String> relevantPrefixes = new TreeSet<>();
    +        for (String loggerName : loggerNames) {
    +            String[] loggerNameParts = loggerName.split("[.]");
    +
    +            String longerPrefix = null;
    +            for (int i = loggerNameParts.length; i > 0; i--) {
    +                String loggerNamePrefix = StringUtils.join(Arrays.copyOf(loggerNameParts, i), ".");
    +                seenPrefixes.put(loggerNamePrefix, seenPrefixes.getOrDefault(loggerNamePrefix, 0) + 1);
    +                if (longerPrefix == null) {
    +                    relevantPrefixes.add(loggerNamePrefix); // actual logger name
    +                    longerPrefix = loggerNamePrefix;
    +                    continue;
    +                }
    +
    +                if (seenPrefixes.get(loggerNamePrefix) > seenPrefixes.get(longerPrefix)) {
    +                    relevantPrefixes.add(loggerNamePrefix);
    +                }
    +                longerPrefix = loggerNamePrefix;
    +            }
    +        }
    +        return relevantPrefixes;
    +    }
    +
         @Restricted(NoExternalUse.class)
         public AutoCompletionCandidates doAutoCompleteLoggerName(@QueryParameter String value) {
    -        AutoCompletionCandidates candidates = new AutoCompletionCandidates();
    -        Enumeration<String> loggerNames = LogManager.getLogManager().getLoggerNames();
    -        while (loggerNames.hasMoreElements()) {
    -            String loggerName = loggerNames.nextElement();
    -            if (loggerName.toLowerCase(Locale.ENGLISH).contains(value.toLowerCase(Locale.ENGLISH))) {
    -                candidates.add(loggerName);
    +        if (value == null) {
    +            return new AutoCompletionCandidates();
    +        }
    +
    +        // get names of all actual loggers known to Jenkins
    +        Set<String> candidateNames = new LinkedHashSet<>(getAutoCompletionCandidates(Collections.list(LogManager.getLogManager().getLoggerNames())));
    +
    +        for (String part : value.split("[ ]+")) {
    +            HashSet<String> partCandidates = new HashSet<>();
    +            String lowercaseValue = part.toLowerCase(Locale.ENGLISH);
    +            for (String loggerName : candidateNames) {
    +                if (loggerName.toLowerCase(Locale.ENGLISH).contains(lowercaseValue)) {
    +                    partCandidates.add(loggerName);
    +                }
                 }
    +            candidateNames.retainAll(partCandidates);
             }
    +        AutoCompletionCandidates candidates = new AutoCompletionCandidates();
    +        candidates.add(candidateNames.toArray(MemoryReductionUtil.EMPTY_STRING_ARRAY));
             return candidates;
         }
     
    diff --git a/core/src/main/java/hudson/logging/LogRecorderManager.java b/core/src/main/java/hudson/logging/LogRecorderManager.java
    index bac8c8b6859a644119397b7b4fc8b2004d53a4d9..765253341bc13b4d9fb501320ddf32bb7db82e20 100644
    --- a/core/src/main/java/hudson/logging/LogRecorderManager.java
    +++ b/core/src/main/java/hudson/logging/LogRecorderManager.java
    @@ -25,6 +25,7 @@ package hudson.logging;
     
     import hudson.FeedAdapter;
     import hudson.Functions;
    +import hudson.PluginManager;
     import hudson.init.Initializer;
     import static hudson.init.InitMilestone.PLUGINS_PREPARED;
     import hudson.model.AbstractModelObject;
    @@ -35,7 +36,10 @@ import jenkins.model.JenkinsLocationConfiguration;
     import jenkins.model.ModelObjectWithChildren;
     import jenkins.model.ModelObjectWithContextMenu.ContextMenu;
     import org.apache.commons.io.filefilter.WildcardFileFilter;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.QueryParameter;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.HttpResponse;
    @@ -61,7 +65,7 @@ import java.util.logging.Logger;
      *
      * @author Kohsuke Kawaguchi
      */
    -public class LogRecorderManager extends AbstractModelObject implements ModelObjectWithChildren {
    +public class LogRecorderManager extends AbstractModelObject implements ModelObjectWithChildren, StaplerProxy {
         /**
          * {@link LogRecorder}s keyed by their {@linkplain LogRecorder#name name}.
          */
    @@ -198,4 +202,19 @@ public class LogRecorderManager extends AbstractModelObject implements ModelObje
         public static void init(Jenkins h) throws IOException {
             h.getLog().load();
         }
    +
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(LogRecorderManager.class.getName() + ".skipPermissionCheck");
     }
    diff --git a/core/src/main/java/hudson/markup/MarkupFormatter.java b/core/src/main/java/hudson/markup/MarkupFormatter.java
    index d722fb6aa248854296df770cdfb58f0af6e9e425..be8004136ab2646a9ea2c7627643ebd778404548 100644
    --- a/core/src/main/java/hudson/markup/MarkupFormatter.java
    +++ b/core/src/main/java/hudson/markup/MarkupFormatter.java
    @@ -51,7 +51,7 @@ import org.kohsuke.stapler.QueryParameter;
      *   
      * <h2>Views</h2>
      * <p>
    - * This extension point must have a valid <tt>config.jelly</tt> that feeds the constructor.
    + * This extension point must have a valid {@code config.jelly} that feeds the constructor.
      *
      * TODO: allow {@link MarkupFormatter} to control the UI that the user uses to edit.
      *
    diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java
    index cf035f6b03ff5b710a0f8069c40fcd8615631c1c..2268f3dd9967c69df673fede3e6bf159c59a28dd 100644
    --- a/core/src/main/java/hudson/model/AbstractBuild.java
    +++ b/core/src/main/java/hudson/model/AbstractBuild.java
    @@ -297,7 +297,7 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
         /**
          * Returns the root directory of the checked-out module.
          * <p>
    -     * This is usually where <tt>pom.xml</tt>, <tt>build.xml</tt>
    +     * This is usually where {@code pom.xml}, {@code build.xml}
          * and so on exists.
          */
         public final FilePath getModuleRoot() {
    @@ -324,6 +324,12 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
             return culprits;
         }
     
    +    @Override
    +    @Exported
    +    @Nonnull public Set<User> getCulprits() {
    +        return RunWithSCM.super.getCulprits();
    +    }
    +
         @Override
         public boolean shouldCalculateCulprits() {
             return getCulpritIds() == null;
    @@ -337,7 +343,7 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
             AbstractBuild<P,R> p = getPreviousCompletedBuild();
             if (upstreamCulprits) {
                 // If we have dependencies since the last successful build, add their authors to our list
    -            if (p.getPreviousNotFailedBuild() != null) {
    +            if (p != null && p.getPreviousNotFailedBuild() != null) {
                     Map<AbstractProject, AbstractBuild.DependencyChange> depmap =
                             p.getDependencyChanges(p.getPreviousSuccessfulBuild());
                     for (AbstractBuild.DependencyChange dep : depmap.values()) {
    @@ -430,11 +436,19 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
             protected Lease decideWorkspace(@Nonnull Node n, WorkspaceList wsl) throws InterruptedException, IOException {
                 String customWorkspace = getProject().getCustomWorkspace();
                 if (customWorkspace != null) {
    +                FilePath rootPath = n.getRootPath();
    +                if (rootPath == null) {
    +                    throw new AbortException(n.getDisplayName() + " seems to be offline");
    +                }
                     // we allow custom workspaces to be concurrently used between jobs.
    -                return Lease.createDummyLease(n.getRootPath().child(getEnvironment(listener).expand(customWorkspace)));
    +                return Lease.createDummyLease(rootPath.child(getEnvironment(listener).expand(customWorkspace)));
                 }
                 // TODO: this cast is indicative of abstraction problem
    -            return wsl.allocate(n.getWorkspaceFor((TopLevelItem)getProject()), getBuild());
    +            FilePath ws = n.getWorkspaceFor((TopLevelItem) getProject());
    +            if (ws == null) {
    +                throw new AbortException(n.getDisplayName() + " seems to be offline");
    +            }
    +            return wsl.allocate(ws, getBuild());
             }
     
             public Result run(@Nonnull BuildListener listener) throws Exception {
    @@ -450,7 +464,7 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
                     if (node instanceof Jenkins) {
                         listener.getLogger().print(Messages.AbstractBuild_BuildingOnMaster());
                     } else {
    -                    listener.getLogger().print(Messages.AbstractBuild_BuildingRemotely(ModelHyperlinkNote.encodeTo("/computer/" + builtOn, builtOn)));
    +                    listener.getLogger().print(Messages.AbstractBuild_BuildingRemotely(ModelHyperlinkNote.encodeTo("/computer/" + builtOn, node.getDisplayName())));
                         Set<LabelAtom> assignedLabels = new HashSet<LabelAtom>(node.getAssignedLabels());
                         assignedLabels.remove(node.getSelfLabel());
                         if (!assignedLabels.isEmpty()) {
    @@ -789,20 +803,6 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
             }
         }
     
    -    /**
    -     * get the fingerprints associated with this build
    -     *
    -     * @return never null
    -     */
    -    @Exported(name = "fingerprint", inline = true, visibility = -1)
    -    public Collection<Fingerprint> getBuildFingerprints() {
    -        FingerprintAction fingerprintAction = getAction(FingerprintAction.class);
    -        if (fingerprintAction != null) {
    -            return fingerprintAction.getFingerprints().values();
    -        }
    -        return Collections.<Fingerprint>emptyList();
    -    }
    -
     	/*
          * No need to lock the entire AbstractBuild on change set calculation
          */
    @@ -1059,7 +1059,7 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
     
                     AbstractBuild<?,?> b = p.getBuildByNumber(i);
                     if (b!=null)
    -                    return Messages.AbstractBuild_KeptBecause(b);
    +                    return Messages.AbstractBuild_KeptBecause(p.hasPermission(Item.READ) ? b.toString() : "?");
                 }
             }
     
    diff --git a/core/src/main/java/hudson/model/AbstractCIBase.java b/core/src/main/java/hudson/model/AbstractCIBase.java
    index 16744334e5866c9256612d9edfd4f2d9263ea5bd..d82afd6488d03d5e52f48abc3347332106d7f3b8 100644
    --- a/core/src/main/java/hudson/model/AbstractCIBase.java
    +++ b/core/src/main/java/hudson/model/AbstractCIBase.java
    @@ -39,6 +39,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     
     import jenkins.model.Configuration;
     
    @@ -125,7 +126,18 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
             } else {
                 // we always need Computer for the master as a fallback in case there's no other Computer.
                 if(n.getNumExecutors()>0 || n==Jenkins.getInstance()) {
    -                computers.put(n, c = n.createComputer());
    +                try {
    +                    c = n.createComputer();
    +                } catch(RuntimeException ex) { // Just in case there is a bogus extension
    +                    LOGGER.log(Level.WARNING, "Error retrieving computer for node " + n.getNodeName() + ", continuing", ex);
    +                }
    +                if (c == null) {
    +                    LOGGER.log(Level.WARNING, "Cannot create computer for node {0}, the {1}#createComputer() method returned null. Skipping this node", 
    +                            new Object[]{n.getNodeName(), n.getClass().getName()});
    +                    return;
    +                }
    +                
    +                computers.put(n, c);
                     if (!n.isHoldOffLaunchUntilSave() && automaticSlaveLaunch) {
                         RetentionStrategy retentionStrategy = c.getRetentionStrategy();
                         if (retentionStrategy != null) {
    @@ -136,8 +148,11 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
                             c.connect(true);
                         }
                     }
    +                used.add(c);
    +            } else {
    +                // TODO: Maybe it should be allowed, but we would just get NPE in the original logic before JENKINS-43496
    +                LOGGER.log(Level.WARNING, "Node {0} has no executors. Cannot update the Computer instance of it", n.getNodeName());
                 }
    -            used.add(c);
             }
         }
     
    @@ -184,15 +199,16 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
                         byName.put(node.getNodeName(),c);
                     }
     
    -                Set<Computer> used = new HashSet<Computer>(old.size());
    +                Set<Computer> used = new HashSet<>(old.size());
     
                     updateComputer(AbstractCIBase.this, byName, used, automaticSlaveLaunch);
                     for (Node s : getNodes()) {
                         long start = System.currentTimeMillis();
                         updateComputer(s, byName, used, automaticSlaveLaunch);
    -                    if(LOG_STARTUP_PERFORMANCE)
    -                        LOGGER.info(String.format("Took %dms to update node %s",
    -                                System.currentTimeMillis()-start, s.getNodeName()));
    +                    if (LOG_STARTUP_PERFORMANCE && LOGGER.isLoggable(Level.FINE)) {
    +                        LOGGER.fine(String.format("Took %dms to update node %s",
    +                                System.currentTimeMillis() - start, s.getNodeName()));
    +                    }
                     }
     
                     // find out what computers are removed, and kill off all executors.
    @@ -211,8 +227,13 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
                 killComputer(c);
             }
             getQueue().scheduleMaintenance();
    -        for (ComputerListener cl : ComputerListener.all())
    -            cl.onConfigurationChange();
    +        for (ComputerListener cl : ComputerListener.all()) {
    +            try {
    +                cl.onConfigurationChange();
    +            } catch (Throwable t) {
    +                LOGGER.log(Level.WARNING, null, t);
    +            }
    +        }
         }
     
     }
    diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java
    index 8cd5a1625e3640bfbc21674f2ad16df1eafd1634..1374429381f550e0e8bddfa991431c794eb4d56b 100644
    --- a/core/src/main/java/hudson/model/AbstractItem.java
    +++ b/core/src/main/java/hudson/model/AbstractItem.java
    @@ -36,12 +36,14 @@ import hudson.model.listeners.ItemListener;
     import hudson.model.listeners.SaveableListener;
     import hudson.model.queue.Tasks;
     import hudson.model.queue.WorkUnit;
    +import hudson.security.ACLContext;
     import hudson.security.AccessControlled;
     import hudson.security.Permission;
     import hudson.security.ACL;
     import hudson.util.AlternativeUiTextProvider;
     import hudson.util.AlternativeUiTextProvider.Message;
     import hudson.util.AtomicFileWriter;
    +import hudson.util.FormValidation;
     import hudson.util.IOUtils;
     import hudson.util.Secret;
     import java.util.Iterator;
    @@ -56,6 +58,8 @@ import jenkins.util.xml.XMLUtils;
     
     import org.apache.tools.ant.taskdefs.Copy;
     import org.apache.tools.ant.types.FileSet;
    +import org.kohsuke.stapler.HttpResponses;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.WebMethod;
     import org.kohsuke.stapler.export.Exported;
     import org.kohsuke.stapler.export.ExportedBean;
    @@ -72,12 +76,16 @@ import java.util.regex.Matcher;
     import java.util.regex.Pattern;
     import javax.annotation.Nonnull;
     
    +import org.acegisecurity.AccessDeniedException;
    +import org.kohsuke.stapler.HttpResponse;
    +import org.kohsuke.stapler.HttpResponses;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.Stapler;
     import org.kohsuke.stapler.HttpDeletable;
     import org.kohsuke.args4j.Argument;
     import org.kohsuke.args4j.CmdLineException;
    +import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.interceptor.RequirePOST;
     import org.xml.sax.SAXException;
     
    @@ -90,6 +98,8 @@ import javax.xml.transform.stream.StreamSource;
     import static hudson.model.queue.Executables.getParentOf;
     import hudson.model.queue.SubTask;
     import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
    +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
    +
     import org.apache.commons.io.FileUtils;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
    @@ -103,7 +113,7 @@ import org.kohsuke.stapler.Ancestor;
     // Item doesn't necessarily have to be Actionable, but
     // Java doesn't let multiple inheritance.
     @ExportedBean
    -public abstract class AbstractItem extends Actionable implements Item, HttpDeletable, AccessControlled, DescriptorByNameOwner {
    +public abstract class AbstractItem extends Actionable implements Item, HttpDeletable, AccessControlled, DescriptorByNameOwner, StaplerProxy {
     
         private static final Logger LOGGER = Logger.getLogger(AbstractItem.class.getName());
     
    @@ -228,6 +238,114 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
             this.name = name;
         }
     
    +    /**
    +     * Controls whether the default rename action is available for this item.
    +     *
    +     * @return whether {@link #name} can be modified by a user
    +     * @see #checkRename
    +     * @see #renameTo
    +     * @since 2.110
    +     */
    +    public boolean isNameEditable() {
    +        return false;
    +    }
    +
    +    /**
    +     * Renames this item
    +     */
    +    @RequirePOST
    +    @Restricted(NoExternalUse.class)
    +    public HttpResponse doConfirmRename(@QueryParameter String newName) throws IOException {
    +        newName = newName == null ? null : newName.trim();
    +        FormValidation validationError = doCheckNewName(newName);
    +        if (validationError.kind != FormValidation.Kind.OK) {
    +            throw new Failure(validationError.getMessage());
    +        }
    +
    +        renameTo(newName);
    +        // send to the new job page
    +        // note we can't use getUrl() because that would pick up old name in the
    +        // Ancestor.getUrl()
    +        return HttpResponses.redirectTo("../" + newName);
    +    }
    +
    +    /**
    +     * Called by {@link #doConfirmRename} and {@code rename.jelly} to validate renames.
    +     * @return {@link FormValidation#ok} if this item can be renamed as specified, otherwise
    +     * {@link FormValidation#error} with a message explaining the problem.
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public @Nonnull FormValidation doCheckNewName(@QueryParameter String newName) {
    +        // TODO: Create an Item.RENAME permission to use here, see JENKINS-18649.
    +        if (!hasPermission(Item.CONFIGURE)) {
    +            if (parent instanceof AccessControlled) {
    +                ((AccessControlled)parent).checkPermission(Item.CREATE);
    +            }
    +            checkPermission(Item.DELETE);
    +        }
    +
    +        newName = newName == null ? null : newName.trim();
    +        try {
    +            Jenkins.checkGoodName(newName);
    +            assert newName != null; // Would have thrown Failure
    +            if (newName.equals(name)) {
    +                return FormValidation.warning(Messages.AbstractItem_NewNameUnchanged());
    +            }
    +            Jenkins.get().getProjectNamingStrategy().checkName(newName);
    +            checkIfNameIsUsed(newName);
    +            checkRename(newName);
    +        } catch (Failure e) {
    +            return FormValidation.error(e.getMessage());
    +        }
    +        return FormValidation.ok();
    +    }
    +
    +    /**
    +     * Check new name for job
    +     * @param newName - New name for job.
    +     */
    +    private void checkIfNameIsUsed(@Nonnull String newName) throws Failure {
    +        try {
    +            Item item = getParent().getItem(newName);
    +            if (item != null) {
    +                throw new Failure(Messages.AbstractItem_NewNameInUse(newName));
    +            }
    +            try (ACLContext ctx = ACL.as(ACL.SYSTEM)) {
    +                item = getParent().getItem(newName);
    +                if (item != null) {
    +                    if (LOGGER.isLoggable(Level.FINE)) {
    +                        LOGGER.log(Level.FINE, "Unable to rename the job {0}: name {1} is already in use. " +
    +                                "User {2} has no {3} permission for existing job with the same name",
    +                                new Object[] {this.getFullName(), newName, ctx.getPreviousContext().getAuthentication().getName(), Item.DISCOVER.name} );
    +                    }
    +                    // Don't explicitly mention that there is another item with the same name.
    +                    throw new Failure(Messages.Jenkins_NotAllowedName(newName));
    +                }
    +            }
    +        } catch(AccessDeniedException ex) {
    +            if (LOGGER.isLoggable(Level.FINE)) {
    +                LOGGER.log(Level.FINE, "Unable to rename the job {0}: name {1} is already in use. " +
    +                        "User {2} has {3} permission, but no {4} for existing job with the same name",
    +                        new Object[] {this.getFullName(), newName, User.current(), Item.DISCOVER.name, Item.READ.name} );
    +            }
    +            throw new Failure(Messages.AbstractItem_NewNameInUse(newName));
    +        }
    +    }
    +
    +    /**
    +     * Allows subclasses to block renames for domain-specific reasons. Generic validation of the new name
    +     * (e.g., null checking, checking for illegal characters, and checking that the name is not in use)
    +     * always happens prior to calling this method.
    +     *
    +     * @param newName the new name for the item
    +     * @throws Failure if the rename should be blocked
    +     * @since 2.110
    +     * @see Job#checkRename
    +     */
    +    protected void checkRename(@Nonnull String newName) throws Failure {
    +
    +    }
    +
         /**
          * Renames this item.
          * Not all the Items need to support this operation, but if you decide to do so,
    @@ -381,21 +499,6 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
             return getRelativeNameFrom(p);
         }
     
    -    /**
    -     * @param p
    -     *  The ItemGroup instance used as context to evaluate the relative name of this AbstractItem
    -     * @return
    -     *  The name of the current item, relative to p.
    -     *  Nested ItemGroups are separated by / character.
    -     */
    -    public String getRelativeNameFrom(ItemGroup p) {
    -        return Functions.getRelativeNameFrom(this, p);
    -    }
    -
    -    public String getRelativeNameFrom(Item item) {
    -        return getRelativeNameFrom(item.getParent());
    -    }
    -
         /**
          * Called right after when a {@link Item} is loaded from disk.
          * This is an opportunity to do a post load processing.
    @@ -470,12 +573,10 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
             return getShortUrl();
         }
     
    +    @Override
         @Exported(visibility=999,name="url")
         public final String getAbsoluteUrl() {
    -        String r = Jenkins.getInstance().getRootUrl();
    -        if(r==null)
    -            throw new IllegalStateException("Root URL isn't configured yet. Cannot compute absolute URL.");
    -        return Util.encode(r+getUrl());
    +        return Item.super.getAbsoluteUrl();
         }
     
         /**
    @@ -492,20 +593,6 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
             return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
         }
     
    -    /**
    -     * Short for {@code getACL().checkPermission(p)}
    -     */
    -    public void checkPermission(Permission p) {
    -        getACL().checkPermission(p);
    -    }
    -
    -    /**
    -     * Short for {@code getACL().hasPermission(p)}
    -     */
    -    public boolean hasPermission(Permission p) {
    -        return getACL().hasPermission(p);
    -    }
    -
         /**
          * Save the settings to a file.
          */
    @@ -519,8 +606,22 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
             return Items.getConfigFile(this);
         }
     
    -    public Descriptor getDescriptorByName(String className) {
    -        return Jenkins.getInstance().getDescriptorByName(className);
    +    private Object writeReplace() {
    +        return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this));
    +    }
    +    private static class Replacer {
    +        private final String fullName;
    +        Replacer(AbstractItem i) {
    +            fullName = i.getFullName();
    +        }
    +        private Object readResolve() {
    +            Jenkins j = Jenkins.getInstanceOrNull();
    +            if (j == null) {
    +                return null;
    +            }
    +            // Will generally only work if called after job loading:
    +            return j.getItemByFullName(fullName);
    +        }
         }
     
         /**
    @@ -692,7 +793,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
         }
     
         /**
    -     * Accepts <tt>config.xml</tt> submission, as well as serve it.
    +     * Accepts {@code config.xml} submission, as well as serve it.
          */
         @WebMethod(name = "config.xml")
         public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp)
    @@ -769,7 +870,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
                 }
     
                 // try to reflect the changes by reloading
    -            Object o = new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this);
    +            Object o = new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshalNullingOut(this);
                 if (o!=this) {
                     // ensure that we've got the same job type. extending this code to support updating
                     // to different job type requires destroying & creating a new job type
    @@ -836,6 +937,24 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
             return super.toString() + '[' + (parent != null ? getFullName() : "?/" + name) + ']';
         }
     
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            if (!getACL().hasPermission(Item.DISCOVER)) {
    +                return null;
    +            }
    +            getACL().checkPermission(Item.READ);
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(AbstractItem.class.getName() + ".skipPermissionCheck");
    +
         /**
          * Used for CLI binding.
          */
    diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java
    index ebb079efb6d1373304d3cea5a6000e654ccef343..3b60e8618519d41c5dc5b6a23a357377296d1d6a 100644
    --- a/core/src/main/java/hudson/model/AbstractProject.java
    +++ b/core/src/main/java/hudson/model/AbstractProject.java
    @@ -75,7 +75,6 @@ import hudson.util.AlternativeUiTextProvider;
     import hudson.util.AlternativeUiTextProvider.Message;
     import hudson.util.DescribableList;
     import hudson.util.FormValidation;
    -import hudson.util.TimeUnit2;
     import hudson.widgets.HistoryWidget;
     import java.io.File;
     import java.io.IOException;
    @@ -92,6 +91,7 @@ import java.util.SortedMap;
     import java.util.TreeMap;
     import java.util.Vector;
     import java.util.concurrent.Future;
    +import java.util.concurrent.TimeUnit;
     import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    @@ -407,6 +407,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
         /**
          * Gets the textual representation of the assigned label as it was entered by the user.
          */
    +    @Exported(name="labelExpression")
         public String getAssignedLabelString() {
             if (canRoam || assignedNode==null)    return null;
             try {
    @@ -572,7 +573,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
         /**
          * Returns the root directory of the checked-out module.
          * <p>
    -     * This is usually where <tt>pom.xml</tt>, <tt>build.xml</tt>
    +     * This is usually where {@code pom.xml}, {@code build.xml}
          * and so on exists.
          *
          * @deprecated as of 1.319
    @@ -641,7 +642,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
         }
     
         /**
    -     * Used in <tt>sidepanel.jelly</tt> to decide whether to display
    +     * Used in {@code sidepanel.jelly} to decide whether to display
          * the config/delete/build links.
          */
         public boolean isConfigurable() {
    @@ -750,7 +751,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
          * Returns the live list of all {@link Publisher}s configured for this project.
          *
          * <p>
    -     * This method couldn't be called <tt>getPublishers()</tt> because existing methods
    +     * This method couldn't be called {@code getPublishers()} because existing methods
          * in sub-classes return different inconsistent types.
          */
         public abstract DescribableList<Publisher,Descriptor<Publisher>> getPublishersList();
    @@ -1015,24 +1016,6 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
             return this; // in this way, any member that wants to run with the main guy can nominate the project itself
         }
     
    -    /**
    -     * {@inheritDoc}
    -     *
    -     * <p>
    -     * A project must be blocked if its own previous build is in progress,
    -     * or if the blockBuildWhenUpstreamBuilding option is true and an upstream
    -     * project is building, but derived classes can also check other conditions.
    -     */
    -    @Override
    -    public boolean isBuildBlocked() {
    -        return getCauseOfBlockage()!=null;
    -    }
    -
    -    public String getWhyBlocked() {
    -        CauseOfBlockage cb = getCauseOfBlockage();
    -        return cb!=null ? cb.getShortDescription() : null;
    -    }
    -
         /**
          * @deprecated use {@link BlockedBecauseOfBuildInProgress} instead.
          */
    @@ -1075,10 +1058,18 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
             }
         }
     
    +    /**
    +     * {@inheritDoc}
    +     *
    +     * <p>
    +     * A project must be blocked if its own previous build is in progress,
    +     * or if the blockBuildWhenUpstreamBuilding option is true and an upstream
    +     * project is building, but derived classes can also check other conditions.
    +     */
         @Override
         public CauseOfBlockage getCauseOfBlockage() {
             // Block builds until they are done with post-production
    -        if (isLogUpdated() && !isConcurrentBuild()) {
    +        if (!isConcurrentBuild() && isLogUpdated()) {
                 final R lastBuild = getLastBuild();
                 if (lastBuild != null) {
                     return new BlockedBecauseOfBuildInProgress(lastBuild);
    @@ -1207,7 +1198,12 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
                 return true;    // no SCM
     
             FilePath workspace = build.getWorkspace();
    -        workspace.mkdirs();
    +        if(workspace!=null){
    +            workspace.mkdirs();
    +        } else {
    +            throw new AbortException("Cannot checkout SCM, workspace is not defined");
    +        }
    +
     
             boolean r = scm.checkout(build, launcher, workspace, listener, changelogFile);
             if (r) {
    @@ -1345,7 +1341,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
                     // However, first there are some conditions in which we do not want to do so.
                     // give time for agents to come online if we are right after reconnection (JENKINS-8408)
                     long running = Jenkins.getInstance().getInjector().getInstance(Uptime.class).getUptime();
    -                long remaining = TimeUnit2.MINUTES.toMillis(10)-running;
    +                long remaining = TimeUnit.MINUTES.toMillis(10)-running;
                     if (remaining>0 && /* this logic breaks tests of polling */!Functions.getIsUnitTest()) {
                         listener.getLogger().print(Messages.AbstractProject_AwaitingWorkspaceToComeOnline(remaining/1000));
                         listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")");
    @@ -1576,16 +1572,38 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
          * Gets the other {@link AbstractProject}s that should be built
          * when a build of this project is completed.
          */
    -    @Exported
         public final List<AbstractProject> getDownstreamProjects() {
             return Jenkins.getInstance().getDependencyGraph().getDownstream(this);
         }
     
    -    @Exported
    +    @Exported(name="downstreamProjects")
    +    @Restricted(DoNotUse.class) // only for exporting
    +    public List<AbstractProject> getDownstreamProjectsForApi() {
    +        List<AbstractProject> r = new ArrayList<>();
    +        for (AbstractProject p : getDownstreamProjects()) {
    +            if (p.hasPermission(Item.READ)) {
    +                r.add(p);
    +            }
    +        }
    +        return r;
    +    }
    +
         public final List<AbstractProject> getUpstreamProjects() {
             return Jenkins.getInstance().getDependencyGraph().getUpstream(this);
         }
     
    +    @Exported(name="upstreamProjects")
    +    @Restricted(DoNotUse.class) // only for exporting
    +    public List<AbstractProject> getUpstreamProjectsForApi() {
    +        List<AbstractProject> r = new ArrayList<>();
    +        for (AbstractProject p : getUpstreamProjects()) {
    +            if (p.hasPermission(Item.READ)) {
    +                r.add(p);
    +            }
    +        }
    +        return r;
    +    }
    +
         /**
          * Returns only those upstream projects that defines {@link BuildTrigger} to this project.
          * This is a subset of {@link #getUpstreamProjects()}
    diff --git a/core/src/main/java/hudson/model/Action.java b/core/src/main/java/hudson/model/Action.java
    index ac7be33a6091c7f9c14d3a02bab31ce7f88c0b45..72d2f84e065afe93aaefceb0f93b530f960c95f3 100644
    --- a/core/src/main/java/hudson/model/Action.java
    +++ b/core/src/main/java/hudson/model/Action.java
    @@ -38,12 +38,12 @@ import javax.annotation.CheckForNull;
      *
      * <p>
      * Some actions use the latter without the former (for example, to add a link to an external website),
    - * while others do the former without the latter (for example, to just draw some graphs in <tt>floatingBox.jelly</tt>),
    + * while others do the former without the latter (for example, to just draw some graphs in {@code floatingBox.jelly}),
      * and still some others do both.
      *
      * <h2>Views</h2>
      * <p>
    - * If an action has a view named <tt>floatingBox.jelly</tt>,
    + * If an action has a view named {@code floatingBox.jelly},
      * it will be displayed as a floating box on the top page of
      * the target {@link ModelObject}. (For example, this is how
      * the JUnit test result trend shows up in the project top page.
    @@ -82,7 +82,7 @@ public interface Action extends ModelObject {
          *
          * @return
          *      If just a file name (like "abc.gif") is returned, it will be
    -     *      interpreted as a file name inside <tt>/images/24x24</tt>.
    +     *      interpreted as a file name inside {@code /images/24x24}.
          *      This is useful for using one of the stock images.
          *      <p>
          *      If an absolute file name that starts from '/' is returned (like
    @@ -91,7 +91,7 @@ public interface Action extends ModelObject {
          *      image files from a plugin.
          *      <p>
          *      Finally, return null to hide it from the task list. This is normally not very useful,
    -     *      but this can be used for actions that only contribute <tt>floatBox.jelly</tt>
    +     *      but this can be used for actions that only contribute {@code floatBox.jelly}
          *      and no task list item. The other case where this is useful is
          *      to avoid showing links that require a privilege when the user is anonymous.
          * @see Functions#isAnonymous()
    diff --git a/core/src/main/java/hudson/model/Actionable.java b/core/src/main/java/hudson/model/Actionable.java
    index 10e894331e95764525c7265f6185b17e03b1315c..96eeda813ab218d96f8603ecc48c651b89d8ccdc 100644
    --- a/core/src/main/java/hudson/model/Actionable.java
    +++ b/core/src/main/java/hudson/model/Actionable.java
    @@ -74,12 +74,15 @@ public abstract class Actionable extends AbstractModelObject implements ModelObj
         @Deprecated
         @Nonnull
         public List<Action> getActions() {
    -        synchronized (this) {
    -            if(actions == null) {
    -                actions = new CopyOnWriteArrayList<Action>();
    +        //this double checked synchronization is only safe if the field 'actions' is volatile
    +        if (actions == null) {
    +            synchronized (this) {
    +                if (actions == null) {
    +                    actions = new CopyOnWriteArrayList<Action>();
    +                }
                 }
    -            return actions;
             }
    +        return actions;
         }
     
         /**
    diff --git a/core/src/main/java/hudson/model/AdministrativeMonitor.java b/core/src/main/java/hudson/model/AdministrativeMonitor.java
    index 537f6d092ce63dd90cac459c1efb2da031ca2f71..2121c3e5991faad2661f1c9536dd6848793b890c 100644
    --- a/core/src/main/java/hudson/model/AdministrativeMonitor.java
    +++ b/core/src/main/java/hudson/model/AdministrativeMonitor.java
    @@ -65,10 +65,10 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
      * <dl>
      * <dt>message.jelly</dt>
      * <dd>
    - * If {@link #isActivated()} returns true, Jenkins will use the <tt>message.jelly</tt>
    + * If {@link #isActivated()} returns true, Jenkins will use the {@code message.jelly}
      * view of this object to render the warning text. This happens in the
    - * <tt>http://SERVER/jenkins/manage</tt> page. This view should typically render
    - * a DIV box with class='error' or class='warning' with a human-readable text
    + * {@code http://SERVER/jenkins/manage} page. This view should typically render
    + * a DIV box with class='alert alert-error' or class='alert alert-warning' with a human-readable text
      * inside it. It often also contains a link to a page that provides more details
      * about the problem.
      * </dd>
    @@ -116,11 +116,11 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
          * Mark this monitor as disabled, to prevent this from showing up in the UI.
          */
         public void disable(boolean value) throws IOException {
    -        AbstractCIBase hudson = Jenkins.getInstance();
    -        Set<String> set = hudson.disabledAdministrativeMonitors;
    +        AbstractCIBase jenkins = Jenkins.get();
    +        Set<String> set = jenkins.disabledAdministrativeMonitors;
             if(value)   set.add(id);
             else        set.remove(id);
    -        hudson.save();
    +        jenkins.save();
         }
     
         /**
    @@ -131,7 +131,7 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
          * he wants to ignore.
          */
         public boolean isEnabled() {
    -        return !((AbstractCIBase)Jenkins.getInstance()).disabledAdministrativeMonitors.contains(id);
    +        return !((AbstractCIBase)Jenkins.get()).disabledAdministrativeMonitors.contains(id);
         }
     
         /**
    @@ -158,7 +158,7 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen
          */
         @Restricted(NoExternalUse.class)
         public Object getTarget() {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +        Jenkins.get().checkPermission(Jenkins.ADMINISTER);
             return this;
         }
     
    diff --git a/core/src/main/java/hudson/model/AllView.java b/core/src/main/java/hudson/model/AllView.java
    index 8db2f352a1a24ff38993a1cf13f520db8ce17122..4daedafd5a92f1e427c4ee542396ba1bb022b370 100644
    --- a/core/src/main/java/hudson/model/AllView.java
    +++ b/core/src/main/java/hudson/model/AllView.java
    @@ -27,7 +27,6 @@ import java.util.List;
     import java.util.Locale;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    -import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import jenkins.util.SystemProperties;
     import org.apache.commons.lang.StringUtils;
    @@ -121,7 +120,7 @@ public class AllView extends View {
          * its name is one of the localized forms of {@link Messages#_Hudson_ViewName()} and the user has not opted out of
          * fixing the view name by setting the system property {@code hudson.mode.AllView.JENKINS-38606} to {@code false}.
          * Use this method to round-trip the primary view name, e.g.
    -     * {@code primaryView = applyJenkins38606Fixup(views, primaryView)}
    +     * {@code primaryView = migrateLegacyPrimaryAllViewLocalizedName(views, primaryView)}
          * <p>
          * NOTE: we can only fix the localized name of an {@link AllView} if it is the primary view as otherwise urls
          * would change, whereas the primary view is special and does not normally get accessed by the
    @@ -164,7 +163,7 @@ public class AllView extends View {
                             // bingo JENKINS-38606 detected
                             LOGGER.log(Level.INFO,
                                     "JENKINS-38606 detected for AllView in {0}; renaming view from {1} to {2}",
    -                                new Object[]{allView.owner.getUrl(), primaryView, DEFAULT_VIEW_NAME});
    +                                new Object[] {allView.owner, primaryView, DEFAULT_VIEW_NAME});
                             allView.name = DEFAULT_VIEW_NAME;
                             return DEFAULT_VIEW_NAME;
                         }
    diff --git a/core/src/main/java/hudson/model/AperiodicWork.java b/core/src/main/java/hudson/model/AperiodicWork.java
    index fbd4f2c4f7a3da987d57e4f61168d453647ff667..3c5385c07791a0c547edd8611c3038014763f137 100644
    --- a/core/src/main/java/hudson/model/AperiodicWork.java
    +++ b/core/src/main/java/hudson/model/AperiodicWork.java
    @@ -24,12 +24,15 @@
     package hudson.model;
     
     import hudson.ExtensionList;
    +import hudson.ExtensionListListener;
     import hudson.ExtensionPoint;
     import hudson.init.Initializer;
     import hudson.triggers.SafeTimerTask;
     import jenkins.util.Timer;
     
    +import java.util.HashSet;
     import java.util.Random;
    +import java.util.Set;
     import java.util.concurrent.TimeUnit;
     import java.util.logging.Logger;
     
    @@ -92,11 +95,17 @@ public abstract class AperiodicWork extends SafeTimerTask implements ExtensionPo
         @Initializer(after= JOB_LOADED)
         public static void init() {
             // start all AperidocWorks
    +        ExtensionList<AperiodicWork> extensionList = all();
    +        extensionList.addListener(new AperiodicWorkExtensionListListener(extensionList));
             for (AperiodicWork p : AperiodicWork.all()) {
    -            Timer.get().schedule(p, p.getInitialDelay(), TimeUnit.MILLISECONDS);
    +            scheduleAperiodWork(p);
             }
         }
     
    +    private static void scheduleAperiodWork(AperiodicWork ap) {
    +        Timer.get().schedule(ap, ap.getInitialDelay(), TimeUnit.MILLISECONDS);
    +    }
    +
         protected abstract void doAperiodicRun();
         
         /**
    @@ -107,4 +116,31 @@ public abstract class AperiodicWork extends SafeTimerTask implements ExtensionPo
         }
     
         private static final Random RANDOM = new Random();
    +
    +    /**
    +     * ExtensionListener that will kick off any new AperiodWork extensions from plugins that are dynamically
    +     * loaded.
    +     */
    +    private static class AperiodicWorkExtensionListListener extends ExtensionListListener {
    +
    +        private final Set<AperiodicWork> registered = new HashSet<>();
    +
    +        AperiodicWorkExtensionListListener(ExtensionList<AperiodicWork> initiallyRegistered) {
    +            for (AperiodicWork p : initiallyRegistered) {
    +                registered.add(p);
    +            }
    +        }
    +
    +        @Override
    +        public void onChange() {
    +            synchronized (registered) {
    +                for (AperiodicWork p : AperiodicWork.all()) {
    +                    if (!registered.contains(p)) {
    +                        scheduleAperiodWork(p);
    +                        registered.add(p);
    +                    }
    +                }
    +            }
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/Api.java b/core/src/main/java/hudson/model/Api.java
    index 6878a4284a1d884c957e1c7aefa8307272f4f504..024d66869d4591afe4d28a31c5294e00b2fa8032 100644
    --- a/core/src/main/java/hudson/model/Api.java
    +++ b/core/src/main/java/hudson/model/Api.java
    @@ -53,12 +53,14 @@ import java.net.HttpURLConnection;
     import java.util.List;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
      * Used to expose remote access API for ".../api/"
      *
      * <p>
    - * If the parent object has a <tt>_api.jelly</tt> view, it will be included
    + * If the parent object has a {@code _api.jelly} view, it will be included
      * in the api index page.
      *
      * @author Kohsuke Kawaguchi
    @@ -133,7 +135,19 @@ public class Api extends AbstractModelObject {
                     XPath comp = dom.createXPath(xpath);
                     comp.setFunctionContext(functionContext);
                     List list = comp.selectNodes(dom);
    +
                     if (wrapper!=null) {
    +                    // check if the wrapper is a valid entity name
    +                    // First position:  letter or underscore
    +                    // Other positions: \w (letter, number, underscore), dash or dot
    +                    String validNameRE = "^[a-zA-Z_][\\w-\\.]*$";
    +
    +                    if(!wrapper.matches(validNameRE)) {
    +                        rsp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    +                        rsp.getWriter().print(Messages.Api_WrapperParamInvalid());
    +                        return;
    +                    }
    +
                         Element root = DocumentFactory.getInstance().createElement(wrapper);
                         for (Object o : list) {
                             if (o instanceof String) {
    @@ -228,7 +242,8 @@ public class Api extends AbstractModelObject {
             return false;
         }
     
    -    private void setHeaders(StaplerResponse rsp) {
    +    @Restricted(NoExternalUse.class)
    +    protected void setHeaders(StaplerResponse rsp) {
             rsp.setHeader("X-Jenkins", Jenkins.VERSION);
             rsp.setHeader("X-Jenkins-Session", Jenkins.SESSION_HASH);
         }
    diff --git a/core/src/main/java/hudson/model/AsyncAperiodicWork.java b/core/src/main/java/hudson/model/AsyncAperiodicWork.java
    index 1d295fa7161db0abcdaffbb957773954b2ec7307..6e720c1419da95663cc16512711c35d6e9ee0486 100644
    --- a/core/src/main/java/hudson/model/AsyncAperiodicWork.java
    +++ b/core/src/main/java/hudson/model/AsyncAperiodicWork.java
    @@ -200,7 +200,7 @@ public abstract class AsyncAperiodicWork extends AperiodicWork {
          * Determines the log file that records the result of this task.
          */
         protected File getLogFile() {
    -        return new File(Jenkins.getActiveInstance().getRootDir(),"logs/tasks/"+name+".log");
    +        return new File(getLogsRoot(), "/tasks/" + name + ".log");
         }
     
         /**
    diff --git a/core/src/main/java/hudson/model/AsyncPeriodicWork.java b/core/src/main/java/hudson/model/AsyncPeriodicWork.java
    index f3ffcc64976a4fa0cbe9892f8c19a3d21a364c1b..324d1853eedc66e2b7425204d1d8b8c0f6630e3d 100644
    --- a/core/src/main/java/hudson/model/AsyncPeriodicWork.java
    +++ b/core/src/main/java/hudson/model/AsyncPeriodicWork.java
    @@ -183,7 +183,7 @@ public abstract class AsyncPeriodicWork extends PeriodicWork {
          * Determines the log file that records the result of this task.
          */
         protected File getLogFile() {
    -        return new File(Jenkins.getActiveInstance().getRootDir(),"logs/tasks/"+name+".log");
    +        return new File(getLogsRoot(), "/tasks/" + name + ".log");
         }
         
         /**
    diff --git a/core/src/main/java/hudson/model/AutoCompletionCandidates.java b/core/src/main/java/hudson/model/AutoCompletionCandidates.java
    index 0373b552bb007f560a566f63573db9ce683296b2..29ce16f4ca59e486771629b9ac0f82b9064d5ab7 100644
    --- a/core/src/main/java/hudson/model/AutoCompletionCandidates.java
    +++ b/core/src/main/java/hudson/model/AutoCompletionCandidates.java
    @@ -25,6 +25,7 @@
     package hudson.model;
     
     import hudson.search.Search;
    +import hudson.search.UserSearchProperty;
     import jenkins.model.Jenkins;
     import org.kohsuke.stapler.HttpResponse;
     import org.kohsuke.stapler.StaplerRequest;
    @@ -37,6 +38,7 @@ import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.List;
     import javax.annotation.CheckForNull;
    +import org.apache.commons.lang.StringUtils;
     
     /**
      * Data representation of the auto-completion candidates.
    @@ -117,22 +119,28 @@ public class AutoCompletionCandidates implements HttpResponse {
     
                 @Override
                 public void onItem(Item i) {
    -                String n = contextualNameOf(i);
    -                if ((n.startsWith(value) || value.startsWith(n))
    +                String itemName = contextualNameOf(i);
    +                
    +                //Check user's setting on whether to do case sensitive comparison, configured in user -> configure
    +                //This is the same setting that is used by the global search field, should be consistent throughout
    +                //the whole application.
    +                boolean caseInsensitive = UserSearchProperty.isCaseInsensitive();
    +
    +                if ((startsWithImpl(itemName, value, caseInsensitive) || startsWithImpl(value, itemName, caseInsensitive))
                         // 'foobar' is a valid candidate if the current value is 'foo'.
                         // Also, we need to visit 'foo' if the current value is 'foo/bar'
    -                 && (value.length()>n.length() || !n.substring(value.length()).contains("/"))
    +                 && (value.length()> itemName.length() || !itemName.substring(value.length()).contains("/"))
                         // but 'foobar/zot' isn't if the current value is 'foo'
                         // we'll first show 'foobar' and then wait for the user to type '/' to show the rest
                      && i.hasPermission(Item.READ)
                         // and read permission required
                     ) {
    -                    if (type.isInstance(i) && n.startsWith(value))
    -                        candidates.add(n);
    +                    if (type.isInstance(i) && startsWithImpl(itemName, value, caseInsensitive))
    +                        candidates.add(itemName);
     
                         // recurse
                         String oldPrefix = prefix;
    -                    prefix = n;
    +                    prefix = itemName;
                         super.onItem(i);
                         prefix = oldPrefix;
                     }
    @@ -161,4 +169,8 @@ public class AutoCompletionCandidates implements HttpResponse {
     
             return candidates;
         }
    +
    +    private static boolean startsWithImpl(String str, String prefix, boolean ignoreCase) {
    +        return ignoreCase ? StringUtils.startsWithIgnoreCase(str, prefix) : str.startsWith(prefix);
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/BuildBadgeAction.java b/core/src/main/java/hudson/model/BuildBadgeAction.java
    index fe35eddd6052cea7c00489eea24e502c4a78378d..3a673f95583521e29e904342ca2a09c148e55cd2 100644
    --- a/core/src/main/java/hudson/model/BuildBadgeAction.java
    +++ b/core/src/main/java/hudson/model/BuildBadgeAction.java
    @@ -32,7 +32,7 @@ package hudson.model;
      * with {@link Run}. 
      *
      * <p>
    - * Actions with this marker should have a view <tt>badge.jelly</tt>,
    + * Actions with this marker should have a view {@code badge.jelly},
      * which will be called to render the badges. The expected visual appearance
      * of a badge is a 16x16 icon.
      *
    diff --git a/core/src/main/java/hudson/model/BuildListener.java b/core/src/main/java/hudson/model/BuildListener.java
    index ed668463de9c4bbc4d6050df6d031bfa6eacfeac..10043b633b22c2301cec142a4aeade9bda4a0e83 100644
    --- a/core/src/main/java/hudson/model/BuildListener.java
    +++ b/core/src/main/java/hudson/model/BuildListener.java
    @@ -23,6 +23,7 @@
      */
     package hudson.model;
     
    +import java.io.PrintStream;
     import java.util.List;
     
     /**
    @@ -38,10 +39,23 @@ public interface BuildListener extends TaskListener {
          * @param causes
          *      Causes that started a build. See {@link Run#getCauses()}.
          */
    -    void started(List<Cause> causes);
    +    default void started(List<Cause> causes) {
    +        PrintStream l = getLogger();
    +        if (causes == null || causes.isEmpty()) {
    +            l.println("Started");
    +        } else {
    +            for (Cause cause : causes) {
    +                // TODO elide duplicates as per CauseAction.getCauseCounts (used in summary.jelly)
    +                cause.print(this);
    +            }
    +        }
    +    }
     
         /**
          * Called when a build is finished.
          */
    -    void finished(Result result);
    +    default void finished(Result result) {
    +        getLogger().println("Finished: " + result);
    +    }
    +
     }
    diff --git a/core/src/main/java/hudson/model/BuildTimelineWidget.java b/core/src/main/java/hudson/model/BuildTimelineWidget.java
    index 31810b5f5e939e8409eb12422b150b3bffb2d91b..717052e5bf9e17d746b63ed15100478c97c35c5c 100644
    --- a/core/src/main/java/hudson/model/BuildTimelineWidget.java
    +++ b/core/src/main/java/hudson/model/BuildTimelineWidget.java
    @@ -23,6 +23,7 @@
      */
     package hudson.model;
     
    +import hudson.Util;
     import hudson.util.RunList;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerRequest;
    @@ -64,7 +65,9 @@ public class BuildTimelineWidget {
                 Event e = new Event();
                 e.start = new Date(r.getStartTimeInMillis());
                 e.end   = new Date(r.getStartTimeInMillis()+r.getDuration());
    -            e.title = r.getFullDisplayName();
    +            // due to SimileAjax.HTML.deEntify (in simile-ajax-bundle.js), "&lt;" are transformed back to "<", but not the "&#60";
    +            // to protect against XSS
    +            e.title = Util.escape(r.getFullDisplayName()).replace("&lt;", "&#60;");
                 // what to put in the description?
                 // e.description = "Longish description of event "+r.getFullDisplayName();
                 // e.durationEvent = true;
    diff --git a/core/src/main/java/hudson/model/BuildableItem.java b/core/src/main/java/hudson/model/BuildableItem.java
    index ef845e11750de8a31ae6ff4c2627ddc6594ceb3f..0d65214e4378535633be61f177c4b2174ad78b52 100644
    --- a/core/src/main/java/hudson/model/BuildableItem.java
    +++ b/core/src/main/java/hudson/model/BuildableItem.java
    @@ -40,13 +40,19 @@ public interface BuildableItem extends Item, Task {
     	 *    Use {@link #scheduleBuild(Cause)}.  Since 1.283
     	 */
         @Deprecated
    -    boolean scheduleBuild();
    +    default boolean scheduleBuild() {
    +    	return scheduleBuild(new Cause.LegacyCodeCause());
    +	}
    +
     	boolean scheduleBuild(Cause c);
     	/**
     	 * @deprecated
     	 *    Use {@link #scheduleBuild(int, Cause)}.  Since 1.283
     	 */
         @Deprecated
    -	boolean scheduleBuild(int quietPeriod);
    +	default boolean scheduleBuild(int quietPeriod) {
    +		return scheduleBuild(quietPeriod, new Cause.LegacyCodeCause());
    +	}
    +
     	boolean scheduleBuild(int quietPeriod, Cause c);
     }
    diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java
    index bca7831012f8ec499187c35bdbd52b134e64b1bb..c0286bc55a8556d88df25c7db3e968938c2ce292 100644
    --- a/core/src/main/java/hudson/model/Cause.java
    +++ b/core/src/main/java/hudson/model/Cause.java
    @@ -25,12 +25,15 @@ package hudson.model;
     
     import java.util.ArrayList;
     import java.util.Arrays;
    +import java.util.Collections;
     import java.util.List;
     
     import hudson.console.ModelHyperlinkNote;
     import hudson.diagnosis.OldDataMonitor;
     import hudson.util.XStream2;
     import jenkins.model.Jenkins;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.DoNotUse;
     import org.kohsuke.stapler.export.Exported;
     import org.kohsuke.stapler.export.ExportedBean;
     import com.thoughtworks.xstream.converters.UnmarshallingContext;
    @@ -364,9 +367,15 @@ public abstract class Cause {
                 this.authenticationName = Jenkins.getAuthentication().getName();
             }
     
    +        /**
    +         * Gets user display name when possible.
    +         * @return User display name.
    +         *         If the User does not exist, returns its ID.
    +         */
             @Exported(visibility=3)
             public String getUserName() {
    -        	return User.get(authenticationName).getDisplayName();
    +        	final User user = User.getById(authenticationName, false);
    +        	return user != null ? user.getDisplayName() : authenticationName;
             }
     
             @Override
    @@ -393,21 +402,48 @@ public abstract class Cause {
          */
         public static class UserIdCause extends Cause {
     
    +        @CheckForNull
             private String userId;
     
    +        /**
    +         * Constructor, which uses the current {@link User}.
    +         */
             public UserIdCause() {
                 User user = User.current();
                 this.userId = (user == null) ? null : user.getId();
             }
     
    +        /**
    +         * Constructor.
    +         * @param userId User ID. {@code null} if the user is unknown.
    +         * @since 2.96
    +         */
    +        public UserIdCause(@CheckForNull String userId) {
    +            this.userId = userId;
    +        }
    +
             @Exported(visibility = 3)
    +        @CheckForNull
             public String getUserId() {
                 return userId;
             }
    +        
    +        @Nonnull
    +        private String getUserIdOrUnknown() {
    +            return  userId != null ? userId : User.getUnknown().getId();
    +        }
     
             @Exported(visibility = 3)
             public String getUserName() {
    -            return userId == null ? "anonymous" : User.get(userId).getDisplayName();
    +            final User user = userId == null ? null : User.getById(userId, false);
    +            return user == null ? "anonymous" : user.getDisplayName();
    +        }
    +
    +        @Restricted(DoNotUse.class) // for Jelly
    +        @CheckForNull
    +        public String getUserUrl() {
    +            final User user = userId == null ? null : User.getById(userId, false);
    +            return user != null ? user.getUrl() : null;
             }
     
             @Override
    @@ -417,9 +453,14 @@ public abstract class Cause {
     
             @Override
             public void print(TaskListener listener) {
    -            listener.getLogger().println(Messages.Cause_UserIdCause_ShortDescription(
    -                    // TODO better to use ModelHyperlinkNote.encodeTo(User), or User.getUrl, since it handles URL escaping
    -                    ModelHyperlinkNote.encodeTo("/user/"+getUserId(), getUserName())));
    +            User user = getUserId() == null ? null : User.getById(getUserId(), false);
    +            if (user != null) {
    +                listener.getLogger().println(Messages.Cause_UserIdCause_ShortDescription(
    +                        ModelHyperlinkNote.encodeTo(user)));
    +            } else {
    +                listener.getLogger().println(Messages.Cause_UserIdCause_ShortDescription(
    +                        "unknown or anonymous"));
    +            }
             }
     
             @Override
    diff --git a/core/src/main/java/hudson/model/ChoiceParameterDefinition.java b/core/src/main/java/hudson/model/ChoiceParameterDefinition.java
    index f175bd001bd0fe08228eb0266fd764c3e4702f32..9ea14153624d8aa3d1d13b2728dec4ccd274a2d6 100644
    --- a/core/src/main/java/hudson/model/ChoiceParameterDefinition.java
    +++ b/core/src/main/java/hudson/model/ChoiceParameterDefinition.java
    @@ -2,6 +2,9 @@ package hudson.model;
     
     import hudson.util.FormValidation;
     import org.jenkinsci.Symbol;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +import org.kohsuke.stapler.DataBoundSetter;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.DataBoundConstructor;
    @@ -10,6 +13,8 @@ import org.apache.commons.lang.StringUtils;
     import net.sf.json.JSONObject;
     import hudson.Extension;
     
    +import javax.annotation.Nonnull;
    +import javax.annotation.Nullable;
     import java.util.ArrayList;
     import java.util.List;
     import java.util.Arrays;
    @@ -24,7 +29,7 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition {
         public static final String CHOICES_DELIMETER = CHOICES_DELIMITER;
     
     
    -    private final List<String> choices;
    +    private /* quasi-final */ List<String> choices;
         private final String defaultValue;
     
         public static boolean areValidChoices(String choices) {
    @@ -32,10 +37,9 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition {
             return !StringUtils.isEmpty(strippedChoices) && strippedChoices.split(CHOICES_DELIMITER).length > 0;
         }
     
    -    @DataBoundConstructor
         public ChoiceParameterDefinition(String name, String choices, String description) {
             super(name, description);
    -        this.choices = Arrays.asList(choices.split(CHOICES_DELIMITER));
    +        setChoicesText(choices);
             defaultValue = null;
         }
     
    @@ -51,6 +55,60 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition {
             this.defaultValue = defaultValue;
         }
     
    +    /**
    +     * Databound constructor for reflective instantiation.
    +     *
    +     * @param name parameter name
    +     * @param description parameter description
    +     *
    +     * @since 2.112
    +     */
    +    @DataBoundConstructor
    +    @Restricted(NoExternalUse.class) // there are specific constructors with String and List arguments for 'choices'
    +    public ChoiceParameterDefinition(String name, String description) {
    +        super(name, description);
    +        this.choices = new ArrayList<>();
    +        this.defaultValue = null;
    +    }
    +
    +    /**
    +     * Set the list of choices. Legal arguments are String (in which case the arguments gets split into lines) and Collection
    +     * which sets the list of legal parameters to the String representations of the argument's non-null entries.
    +     *
    +     * See JENKINS-26143 for background.
    +     *
    +     * This retains the compatibility with the legacy String 'choices' parameter, while supporting the list type as generated
    +     * by the snippet generator.
    +     *
    +     * @param choices String or Collection representing this parameter definition's possible values.
    +     *
    +     * @since 2.112
    +     *
    +     */
    +    @DataBoundSetter
    +    @Restricted(NoExternalUse.class) // this is terrible enough without being used anywhere
    +    public void setChoices(Object choices) {
    +        if (choices instanceof String) {
    +            setChoicesText((String) choices);
    +            return;
    +        }
    +        if (choices instanceof List) {
    +            ArrayList<String> newChoices = new ArrayList<>();
    +            for (Object o : (List) choices) {
    +                if (o != null) {
    +                    newChoices.add(o.toString());
    +                }
    +            }
    +            this.choices = newChoices;
    +            return;
    +        }
    +        throw new IllegalArgumentException("expected String or List, but got " + choices.getClass().getName());
    +    }
    +
    +    private void setChoicesText(String choices) {
    +        this.choices = Arrays.asList(choices.split(CHOICES_DELIMITER));
    +    }
    +
         @Override
         public ParameterDefinition copyWithDefaultValue(ParameterValue defaultValue) {
             if (defaultValue instanceof StringParameterValue) {
    @@ -104,6 +162,17 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition {
                 return "/help/parameter/choice.html";
             }
     
    +        @Override
    +        /*
    +         * We need this for JENKINS-26143 -- reflective creation cannot handle setChoices(Object). See that method for context.
    +         */
    +        public ParameterDefinition newInstance(@Nullable StaplerRequest req, @Nonnull JSONObject formData) throws FormException {
    +            String name = formData.getString("name");
    +            String desc = formData.getString("description");
    +            String choiceText = formData.getString("choices");
    +            return new ChoiceParameterDefinition(name, choiceText, desc);
    +        }
    +
             /**
              * Checks if parameterized build choices are valid.
              */
    diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java
    index 2ac0b2a107f17a7a1319d3c1635cf6b6ad76d046..3a9fde57dc5ef80f73bd2550053652e0479486ec 100644
    --- a/core/src/main/java/hudson/model/Computer.java
    +++ b/core/src/main/java/hudson/model/Computer.java
    @@ -30,9 +30,9 @@ import hudson.EnvVars;
     import hudson.Extension;
     import hudson.Launcher.ProcStarter;
     import hudson.slaves.Cloud;
    +import jenkins.security.stapler.StaplerDispatchable;
     import jenkins.util.SystemProperties;
     import hudson.Util;
    -import hudson.cli.declarative.CLIMethod;
     import hudson.cli.declarative.CLIResolver;
     import hudson.console.AnnotatedLargeText;
     import hudson.init.Initializer;
    @@ -66,8 +66,11 @@ import hudson.util.Futures;
     import hudson.util.NamingThreadFactory;
     import jenkins.model.Jenkins;
     import jenkins.util.ContextResettingExecutorService;
    +import jenkins.util.SystemProperties;
     import jenkins.security.MasterToSlaveCallable;
    +import jenkins.security.ImpersonatingExecutorService;
     
    +import org.apache.commons.lang.StringUtils;
     import org.jenkins.ui.icon.Icon;
     import org.jenkins.ui.icon.IconSet;
     import org.kohsuke.accmod.Restricted;
    @@ -85,7 +88,6 @@ import org.kohsuke.stapler.HttpRedirect;
     import org.kohsuke.stapler.WebMethod;
     import org.kohsuke.stapler.export.Exported;
     import org.kohsuke.stapler.export.ExportedBean;
    -import org.kohsuke.args4j.Option;
     import org.kohsuke.stapler.interceptor.RequirePOST;
     
     import javax.annotation.OverridingMethodsMustInvokeSuper;
    @@ -147,7 +149,7 @@ import static javax.servlet.http.HttpServletResponse.*;
      * @author Kohsuke Kawaguchi
      */
     @ExportedBean
    -public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener {
    +public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener, DescriptorByNameOwner {
     
         private final CopyOnWriteArrayList<Executor> executors = new CopyOnWriteArrayList<Executor>();
         // TODO:
    @@ -325,6 +327,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
          * Used to URL-bind {@link AnnotatedLargeText}.
          */
         public AnnotatedLargeText<Computer> getLogText() {
    +        checkPermission(CONNECT);
             return new AnnotatedLargeText<Computer>(getLogFile(), Charset.defaultCharset(), false, this);
         }
     
    @@ -332,14 +335,6 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
             return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
         }
     
    -    public void checkPermission(Permission permission) {
    -        getACL().checkPermission(permission);
    -    }
    -
    -    public boolean hasPermission(Permission permission) {
    -        return getACL().hasPermission(permission);
    -    }
    -
         /**
          * If the computer was offline (either temporarily or not),
          * this method will return the cause.
    @@ -784,6 +779,12 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
             return "computer/" + Util.rawEncode(getName()) + "/";
         }
     
    +    @Exported
    +    public Set<LabelAtom> getAssignedLabels() {
    +        Node node = getNode();
    +        return (node != null) ? node.getAssignedLabels() : Collections.EMPTY_SET;
    +    }
    +
         /**
          * Returns projects that are tied on this node.
          */
    @@ -907,6 +908,9 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
         }
     
         private void addNewExecutorIfNecessary() {
    +        if (Jenkins.getInstanceOrNull() == null) {
    +            return;
    +        }
             Set<Integer> availableNumbers  = new HashSet<Integer>();
             for (int i = 0; i < numExecutors; i++)
                 availableNumbers.add(i);
    @@ -960,6 +964,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
          * Gets the read-only snapshot view of all {@link Executor}s.
          */
         @Exported
    +    @StaplerDispatchable
         public List<Executor> getExecutors() {
             return new ArrayList<Executor>(executors);
         }
    @@ -968,6 +973,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
          * Gets the read-only snapshot view of all {@link OneOffExecutor}s.
          */
         @Exported
    +    @StaplerDispatchable
         public List<OneOffExecutor> getOneOffExecutors() {
             return new ArrayList<OneOffExecutor>(oneOffExecutors);
         }
    @@ -976,7 +982,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
          * Gets the read-only snapshot view of all {@link Executor} instances including {@linkplain OneOffExecutor}s.
          *
          * @return the read-only snapshot view of all {@link Executor} instances including {@linkplain OneOffExecutor}s.
    -     * @since TODO
    +     * @since 2.55
          */
         public List<Executor> getAllExecutors() {
             List<Executor> result = new ArrayList<>(executors.size() + oneOffExecutors.size());
    @@ -1067,6 +1073,17 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
             return firstDemand;
         }
     
    +    /**
    +     * Returns the {@link Node} description for this computer
    +     */
    +    @Restricted(DoNotUse.class)
    +    @Exported
    +    public @Nonnull String getDescription() {
    +        Node node = getNode();
    +        return (node != null) ? node.getNodeDescription() : null;
    +    }
    +
    +
         /**
          * Called by {@link Executor} to kill excessive executors from this computer.
          */
    @@ -1344,9 +1361,11 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
         }
     
         public static final ExecutorService threadPoolForRemoting = new ContextResettingExecutorService(
    +        new ImpersonatingExecutorService(
                 Executors.newCachedThreadPool(
    -                    new ExceptionCatchingThreadFactory(
    -                            new NamingThreadFactory(new DaemonThreadFactory(), "Computer.threadPoolForRemoting"))));
    +                new ExceptionCatchingThreadFactory(
    +                    new NamingThreadFactory(
    +                        new DaemonThreadFactory(), "Computer.threadPoolForRemoting"))), ACL.SYSTEM));
     
     //
     //
    @@ -1417,10 +1436,11 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
     
         private static final class DumpExportTableTask extends MasterToSlaveCallable<String,IOException> {
             public String call() throws IOException {
    +            final Channel ch = getChannelOrFail();
                 StringWriter sw = new StringWriter();
    -            PrintWriter pw = new PrintWriter(sw);
    -            Channel.current().dumpExportTable(pw);
    -            pw.close();
    +            try (PrintWriter pw = new PrintWriter(sw)) {
    +                ch.dumpExportTable(pw);
    +            }
                 return sw.toString();
             }
         }
    @@ -1464,6 +1484,11 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
                 throw new FormException(Messages.ComputerSet_SlaveAlreadyExists(proposedName), "name");
             }
     
    +        String nExecutors = req.getSubmittedForm().getString("numExecutors");
    +        if (StringUtils.isBlank(nExecutors) || Integer.parseInt(nExecutors)<=0) {
    +            throw new FormException(Messages.Slave_InvalidConfig_Executors(nodeName), "numExecutors");
    +        }
    +
             Node result = node.reconfigure(req, req.getSubmittedForm());
             Jenkins.getInstance().getNodesObject().replaceNode(this.getNode(), result);
     
    @@ -1472,7 +1497,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
         }
     
         /**
    -     * Accepts <tt>config.xml</tt> submission, as well as serve it.
    +     * Accepts {@code config.xml} submission, as well as serve it.
          */
         @WebMethod(name = "config.xml")
         public void doConfigDotXml(StaplerRequest req, StaplerResponse rsp)
    diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java
    index 1de0c37080539a44a75b84807b6f3d76d24a7061..b7fc8097e8950523b3bd10cced0d698a5aea7dd4 100644
    --- a/core/src/main/java/hudson/model/Descriptor.java
    +++ b/core/src/main/java/hudson/model/Descriptor.java
    @@ -39,6 +39,7 @@ import hudson.views.ListViewColumn;
     import jenkins.model.GlobalConfiguration;
     import jenkins.model.GlobalConfigurationCategory;
     import jenkins.model.Jenkins;
    +import jenkins.security.RedactSecretJsonInErrorMessageSanitizer;
     import jenkins.util.io.OnMaster;
     import net.sf.json.JSONArray;
     import net.sf.json.JSONObject;
    @@ -51,6 +52,8 @@ import org.apache.commons.io.IOUtils;
     
     import static hudson.util.QuotedStringTokenizer.*;
     import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
    +
    +import javax.annotation.PostConstruct;
     import javax.servlet.ServletException;
     import javax.servlet.RequestDispatcher;
     import java.io.File;
    @@ -117,6 +120,8 @@ import javax.annotation.Nullable;
      * {@link Descriptor} can persist data just by storing them in fields.
      * However, it is the responsibility of the derived type to properly
      * invoke {@link #save()} and {@link #load()}.
    + * {@link #load()} is automatically invoked as a JSR-250 lifecycle method if derived class
    + * do implement {@link PersistentDescriptor}.
      *
      * <h2>Reflection Enhancement</h2>
      * {@link Descriptor} defines addition to the standard Java reflection
    @@ -133,7 +138,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
          */
         public transient final Class<? extends T> clazz;
     
    -    private transient final Map<String,CheckMethod> checkMethods = new ConcurrentHashMap<String,CheckMethod>();
    +    private transient final Map<String,CheckMethod> checkMethods = new ConcurrentHashMap<String,CheckMethod>(2);
     
         /**
          * Lazily computed list of properties on {@link #clazz} and on the descriptor itself.
    @@ -229,7 +234,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
          *
          * @see #getHelpFile(String) 
          */
    -    private transient final Map<String,HelpRedirect> helpRedirect = new HashMap<String,HelpRedirect>();
    +    private transient final Map<String,HelpRedirect> helpRedirect = new HashMap<String,HelpRedirect>(2);
     
         private static class HelpRedirect {
             private final Class<? extends Describable> owner;
    @@ -534,7 +539,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
          * Creates a configured instance from the submitted form.
          *
          * <p>
    -     * Hudson only invokes this method when the user wants an instance of <tt>T</tt>.
    +     * Hudson only invokes this method when the user wants an instance of {@code T}.
          * So there's no need to check that in the implementation.
          *
          * <p>
    @@ -597,7 +602,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
             } catch (NoSuchMethodException e) {
                 throw new AssertionError(e); // impossible
             } catch (InstantiationException | IllegalAccessException | RuntimeException e) {
    -            throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
    +            throw new Error("Failed to instantiate "+clazz+" from "+RedactSecretJsonInErrorMessageSanitizer.INSTANCE.sanitize(formData),e);
             }
         }
     
    @@ -710,9 +715,9 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
          *
          * <p>
          * This value is relative to the context root of Hudson, so normally
    -     * the values are something like <tt>"/plugin/emma/help.html"</tt> to
    -     * refer to static resource files in a plugin, or <tt>"/publisher/EmmaPublisher/abc"</tt>
    -     * to refer to Jelly script <tt>abc.jelly</tt> or a method <tt>EmmaPublisher.doAbc()</tt>.
    +     * the values are something like {@code "/plugin/emma/help.html"} to
    +     * refer to static resource files in a plugin, or {@code "/publisher/EmmaPublisher/abc"}
    +     * to refer to Jelly script {@code abc.jelly} or a method {@code EmmaPublisher.doAbc()}.
          *
          * @return
          *      null to indicate that there's no help.
    @@ -821,7 +826,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
          *
          * @since 2.0, used to be in {@link GlobalConfiguration} before that.
          */
    -    public GlobalConfigurationCategory getCategory() {
    +    public @Nonnull GlobalConfigurationCategory getCategory() {
             return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Unclassified.class);
         }
     
    @@ -911,7 +916,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
         }
     
         /**
    -     * Serves <tt>help.html</tt> from the resource of {@link #clazz}.
    +     * Serves {@code help.html} from the resource of {@link #clazz}.
          */
         public void doHelp(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
             String path = req.getRestOfPath();
    @@ -984,7 +989,14 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable,
         Map<Descriptor<T>,T> toMap(Iterable<T> describables) {
             Map<Descriptor<T>,T> m = new LinkedHashMap<Descriptor<T>,T>();
             for (T d : describables) {
    -            m.put(d.getDescriptor(),d);
    +            Descriptor<T> descriptor;
    +            try {
    +                descriptor = d.getDescriptor();
    +            } catch (Throwable x) {
    +                LOGGER.log(Level.WARNING, null, x);
    +                continue;
    +            }
    +            m.put(descriptor, d);
             }
             return m;
         }
    diff --git a/core/src/main/java/hudson/model/DescriptorByNameOwner.java b/core/src/main/java/hudson/model/DescriptorByNameOwner.java
    index 0f8d5b26c5cdacd4c69037f903caac52d3b29c3f..26414ca6a155ad4c8d3feb76548ce920a8d7fe66 100644
    --- a/core/src/main/java/hudson/model/DescriptorByNameOwner.java
    +++ b/core/src/main/java/hudson/model/DescriptorByNameOwner.java
    @@ -23,6 +23,8 @@
      */
     package hudson.model;
     
    +import jenkins.model.Jenkins;
    +
     /**
      * Adds {@link #getDescriptorByName(String)} to bind {@link Descriptor}s to URL.
      * Binding them at some specific object (instead of {@link jenkins.model.Jenkins}), allows
    @@ -46,5 +48,7 @@ public interface DescriptorByNameOwner extends ModelObject {
          * @param id
          *      Either {@link Descriptor#getId()} (recommended) or the short name.
          */
    -    Descriptor getDescriptorByName(String id);    
    +    default Descriptor getDescriptorByName(String id) {
    +        return Jenkins.getInstance().getDescriptorByName(id);
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java
    index 5426c7842e115bc9e947c1c243b849412ae6ff6a..4a9217ffcfda4c8b788e6286b4cb0c9909e19a9e 100644
    --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java
    +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java
    @@ -23,22 +23,30 @@
      */
     package hudson.model;
     
    +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     import hudson.FilePath;
     import hudson.Util;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.OutputStream;
     import java.io.Serializable;
    +import java.net.URL;
     import java.text.Collator;
     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.GregorianCalendar;
     import java.util.List;
     import java.util.Locale;
    +import java.util.Objects;
     import java.util.StringTokenizer;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +import java.util.stream.Collectors;
    +import java.util.stream.Stream;
     import javax.servlet.ServletException;
     import javax.servlet.http.HttpServletResponse;
     import jenkins.model.Jenkins;
    @@ -51,6 +59,7 @@ import org.apache.tools.zip.ZipOutputStream;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.HttpResponse;
    +import org.kohsuke.stapler.HttpResponses;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     
    @@ -64,6 +73,9 @@ import org.kohsuke.stapler.StaplerResponse;
      * @author Kohsuke Kawaguchi
      */
     public final class DirectoryBrowserSupport implements HttpResponse {
    +    // escape hatch for SECURITY-904 to keep legacy behavior
    +    @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
    +    public static boolean ALLOW_SYMLINK_ESCAPE = Boolean.getBoolean(DirectoryBrowserSupport.class.getName() + ".allowSymlinkEscape");
     
         public final ModelObject owner;
         
    @@ -212,13 +224,19 @@ public final class DirectoryBrowserSupport implements HttpResponse {
             String base = _base.toString();
             String rest = _rest.toString();
     
    +        if(!ALLOW_SYMLINK_ESCAPE && (root.supportIsDescendant() && !root.isDescendant(base))){
    +            LOGGER.log(Level.WARNING, "Trying to access a file outside of the directory, target: "+ base);
    +            rsp.sendError(HttpServletResponse.SC_FORBIDDEN, "Trying to access a file outside of the directory, target: " + base);
    +            return;
    +        }
    +
             // this is the base file/directory
    -        VirtualFile baseFile = root.child(base);
    +        VirtualFile baseFile = base.isEmpty() ? root : root.child(base);
     
             if(baseFile.isDirectory()) {
                 if(zip) {
                     rsp.setContentType("application/zip");
    -                zip(rsp.getOutputStream(), baseFile, rest);
    +                zip(rsp, root, baseFile, rest);
                     return;
                 }
                 if (plain) {
    @@ -246,8 +264,8 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                 }
     
                 List<List<Path>> glob = null;
    -
    -            if(rest.length()>0) {
    +            boolean patternUsed = rest.length() > 0;
    +            if(patternUsed) {
                     // the rest is Ant glob pattern
                     glob = patternScan(baseFile, rest, createBackRef(restSize));
                 } else
    @@ -257,13 +275,15 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                 }
     
                 if(glob!=null) {
    +                List<List<Path>> filteredGlob = keepReadabilityOnlyOnDescendants(baseFile, patternUsed, glob);
    +                
                     // serve glob
                     req.setAttribute("it", this);
                     List<Path> parentPaths = buildParentPath(base,restSize);
                     req.setAttribute("parentPath",parentPaths);
                     req.setAttribute("backPath", createBackRef(restSize));
                     req.setAttribute("topPath", createBackRef(parentPaths.size()+restSize));
    -                req.setAttribute("files", glob);
    +                req.setAttribute("files", filteredGlob);
                     req.setAttribute("icon", icon);
                     req.setAttribute("path", path);
                     req.setAttribute("pattern",rest);
    @@ -295,6 +315,14 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                 return;
             }
     
    +        URL external = baseFile.toExternalURL();
    +        if (external != null) {
    +            // or this URL could be emitted directly from dir.jelly
    +            // though we would prefer to delay toExternalURL calls unless and until needed
    +            rsp.sendRedirect2(external.toExternalForm());
    +            return;
    +        }
    +
             long lastModified = baseFile.lastModified();
             long length = baseFile.length();
     
    @@ -319,6 +347,57 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                 rsp.serveFile(req, in, lastModified, -1, length, baseFile.getName() );
             }
         }
    +    
    +    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());
    +            String relativePath = "";
    +            for (int i = 0; i < pathFragments.size(); i++) {
    +                Path current = pathFragments.get(i);
    +                if (i == 0) {
    +                    relativePath = current.title;
    +                } else {
    +                    relativePath += "/" + current.title;
    +                }
    +            
    +                if (!current.isReadable) {
    +                    if (patternUsed) {
    +                        // we do not want to leak information about existence of folders / files satisfying the pattern inside that folder
    +                        return null;
    +                    }
    +                    mappedFragments.add(current);
    +                    return mappedFragments;
    +                } else {
    +                    if (isDescendant(root, relativePath)) {
    +                        mappedFragments.add(current);
    +                    } else {
    +                        if (patternUsed) {
    +                            // we do not want to leak information about existence of folders / files satisfying the pattern inside that folder
    +                            return null;
    +                        }
    +                        mappedFragments.add(Path.createNotReadableVersionOf(current));
    +                        return mappedFragments;
    +                    }
    +                }
    +            }
    +            return mappedFragments;
    +        });
    +    
    +        if (patternUsed) {
    +            pathFragmentsStream = pathFragmentsStream.filter(Objects::nonNull);
    +        }
    +        
    +        return pathFragmentsStream.collect(Collectors.toList());
    +    }
    +
    +    private boolean isDescendant(VirtualFile root, String relativePath){
    +        try {
    +            return ALLOW_SYMLINK_ESCAPE || !root.supportIsDescendant() || root.isDescendant(relativePath);
    +        }
    +        catch (IOException e) {
    +            return false;
    +        }
    +    }
     
         private String getPath(StaplerRequest req) {
             String path = req.getRestOfPath();
    @@ -338,7 +417,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
             int current=1;
             while(tokens.hasMoreTokens()) {
                 String token = tokens.nextToken();
    -            r.add(new Path(createBackRef(total-current+restSize),token,true,0, true));
    +            r.add(new Path(createBackRef(total-current+restSize),token,true,0, true,0));
                 current++;
             }
             return r;
    @@ -352,10 +431,12 @@ public final class DirectoryBrowserSupport implements HttpResponse {
             return buf.toString();
         }
     
    -    private static void zip(OutputStream outputStream, VirtualFile dir, String glob) throws IOException {
    +    private static void zip(StaplerResponse rsp, VirtualFile root, VirtualFile dir, String glob) throws IOException, InterruptedException {
    +        OutputStream outputStream = rsp.getOutputStream();
             try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
                 zos.setEncoding(System.getProperty("file.encoding")); // TODO JENKINS-20663 make this overridable via query parameter
    -            for (String n : dir.list(glob.length() == 0 ? "**" : glob)) {
    +            // TODO consider using run(Callable) here
    +            for (String n : dir.list(glob.isEmpty() ? "**" : glob, null, /* TODO what is the user expectation? */true)) {
                     String relativePath;
                     if (glob.length() == 0) {
                         // JENKINS-19947: traditional behavior is to prepend the directory name
    @@ -363,18 +444,24 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                     } else {
                         relativePath = n;
                     }
    -                // 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)
    -                //      but not the file separator char of the (maybe remote) "dir".
    -                ZipEntry e = new ZipEntry(relativePath.replace('\\', '/'));
    -                VirtualFile f = dir.child(n);
    -                e.setTime(f.lastModified());
    -                zos.putNextEntry(e);
    -                try (InputStream in = f.open()) {
    -                    IOUtils.copy(in, zos);
    +
    +                String targetFile = dir.toString().substring(root.toString().length()) + n;
    +                if (!ALLOW_SYMLINK_ESCAPE && root.supportIsDescendant() && !root.isDescendant(targetFile)) {
    +                    LOGGER.log(Level.INFO, "Trying to access a file outside of the directory: " + root + ", illicit target: " + targetFile);
    +                } else {
    +                    // 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)
    +                    //      but not the file separator char of the (maybe remote) "dir".
    +                    ZipEntry e = new ZipEntry(relativePath.replace('\\', '/'));
    +                    VirtualFile f = dir.child(n);
    +                    e.setTime(f.lastModified());
    +                    zos.putNextEntry(e);
    +                    try (InputStream in = f.open()) {
    +                        IOUtils.copy(in, zos);
    +                    }
    +                    zos.closeEntry();
                     }
    -                zos.closeEntry();
                 }
             }
         }
    @@ -404,12 +491,26 @@ public final class DirectoryBrowserSupport implements HttpResponse {
              */
             private final boolean isReadable;
     
    +       /**
    +        * For a file, the last modified timestamp.
    +        */
    +        private final long lastModified;
    +
    +        /**
    +         * @deprecated Use {@link #Path(String, String, boolean, long, boolean, long)}
    +         */
    +        @Deprecated
             public Path(String href, String title, boolean isFolder, long size, boolean isReadable) {
    +            this(href, title, isFolder, size, isReadable, 0L);
    +        }
    +
    +        public Path(String href, String title, boolean isFolder, long size, boolean isReadable, long lastModified) {
                 this.href = href;
                 this.title = title;
                 this.isFolder = isFolder;
                 this.size = size;
                 this.isReadable = isReadable;
    +            this.lastModified = lastModified;
             }
     
             public boolean isFolder() {
    @@ -446,6 +547,33 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                 return size;
             }
     
    +        /**
    +         *
    +         * @return A long value representing the time the file was last modified, measured in milliseconds since
    +         * the epoch (00:00:00 GMT, January 1, 1970), or 0L if is not possible to obtain the times.
    +         * @since 2.127
    +         */
    +        public long getLastModified() {
    +            return lastModified;
    +        }
    +
    +        /**
    +         *
    +         * @return A Calendar representing the time the file was last modified, it lastModified is 0L
    +         * it will return 00:00:00 GMT, January 1, 1970.
    +         * @since 2.127
    +         */
    +        @Restricted(NoExternalUse.class)
    +        public Calendar getLastModifiedAsCalendar() {
    +            final Calendar cal = new GregorianCalendar();
    +            cal.setTimeInMillis(lastModified);
    +            return cal;
    +        }
    +
    +        public static Path createNotReadableVersionOf(Path that){
    +            return new Path(that.href, that.title, that.isFolder, that.size, false);
    +        }
    +
             private static final long serialVersionUID = 1L;
         }
     
    @@ -499,7 +627,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                     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());
    +                    Path p = new Path(Util.rawEncode(f.getName()), f.getName(), f.isDirectory(), f.length(), f.canRead(), f.lastModified());
                         if(!f.isDirectory()) {
                             r.add(Collections.singletonList(p));
                         } else {
    @@ -520,7 +648,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                                     break;
                                 f = sub.get(0);
                                 relPath += '/'+Util.rawEncode(f.getName());
    -                            l.add(new Path(relPath,f.getName(),true,0, f.canRead()));
    +                            l.add(new Path(relPath,f.getName(),true, f.length(), f.canRead(), f.lastModified()));
                             }
                             r.add(l);
                         }
    @@ -535,10 +663,10 @@ public final class DirectoryBrowserSupport implements HttpResponse {
          * @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 {
    -            String[] files = baseDir.list(pattern);
    +            Collection<String> files = baseDir.list(pattern, null, /* TODO what is the user expectation? */true);
     
    -            if (files.length > 0) {
    -                List<List<Path>> r = new ArrayList<List<Path>>(files.length);
    +            if (!files.isEmpty()) {
    +                List<List<Path>> r = new ArrayList<List<Path>>(files.size());
                     for (String match : files) {
                         List<Path> file = buildPathList(baseDir, baseDir.child(match), baseRef);
                         r.add(file);
    @@ -574,7 +702,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
                     href.append("/");
                 }
     
    -            Path path = new Path(href.toString(), filePath.getName(), filePath.isDirectory(), filePath.length(), filePath.canRead());
    +            Path path = new Path(href.toString(), filePath.getName(), filePath.isDirectory(), filePath.length(), filePath.canRead(), filePath.lastModified());
                 pathList.add(path);
             }
     
    diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java
    index ad26b130ae1023751072ff1b7e50f2e27b7a991c..ada651bbe4c6b723538606f9d67ed173a7724d88 100644
    --- a/core/src/main/java/hudson/model/DownloadService.java
    +++ b/core/src/main/java/hudson/model/DownloadService.java
    @@ -35,12 +35,14 @@ import hudson.util.FormValidation;
     import hudson.util.FormValidation.Kind;
     import hudson.util.QuotedStringTokenizer;
     import hudson.util.TextFile;
    -import static hudson.util.TimeUnit2.DAYS;
    +import static java.util.concurrent.TimeUnit.DAYS;
     import java.io.File;
     import java.io.IOException;
     import java.io.InputStream;
     import java.lang.reflect.Field;
    +import java.net.HttpURLConnection;
     import java.net.URL;
    +import java.net.URLConnection;
     import java.net.URLEncoder;
     import java.util.ArrayList;
     import java.util.List;
    @@ -130,18 +132,6 @@ public class DownloadService extends PageDecorator {
         }
     
         private String mapHttps(String url) {
    -        /*
    -            HACKISH:
    -
    -            Loading scripts in HTTP from HTTPS pages cause browsers to issue a warning dialog.
    -            The elegant way to solve the problem is to always load update center from HTTPS,
    -            but our backend mirroring scheme isn't ready for that. So this hack serves regular
    -            traffic in HTTP server, and only use HTTPS update center for Jenkins in HTTPS.
    -
    -            We'll monitor the traffic to see if we can sustain this added traffic.
    -         */
    -        if (url.startsWith("http://updates.jenkins-ci.org/") && Jenkins.getInstance().isRootUrlSecure())
    -            return "https"+url.substring(4);
             return url;
         }
     
    @@ -169,7 +159,12 @@ public class DownloadService extends PageDecorator {
          */
         @Restricted(NoExternalUse.class)
         public static String loadJSON(URL src) throws IOException {
    -        try (InputStream is = ProxyConfiguration.open(src).getInputStream()) {
    +        URLConnection con = ProxyConfiguration.open(src);
    +        if (con instanceof HttpURLConnection) {
    +            // prevent problems from misbehaving plugins disabling redirects by default
    +            ((HttpURLConnection) con).setInstanceFollowRedirects(true);
    +        }
    +        try (InputStream is = con.getInputStream()) {
                 String jsonp = IOUtils.toString(is, "UTF-8");
                 int start = jsonp.indexOf('{');
                 int end = jsonp.lastIndexOf('}');
    @@ -189,7 +184,12 @@ public class DownloadService extends PageDecorator {
          */
         @Restricted(NoExternalUse.class)
         public static String loadJSONHTML(URL src) throws IOException {
    -        try (InputStream is = ProxyConfiguration.open(src).getInputStream()) {
    +        URLConnection con = ProxyConfiguration.open(src);
    +        if (con instanceof HttpURLConnection) {
    +            // prevent problems from misbehaving plugins disabling redirects by default
    +            ((HttpURLConnection) con).setInstanceFollowRedirects(true);
    +        }
    +        try (InputStream is = con.getInputStream()) {
                 String jsonp = IOUtils.toString(is, "UTF-8");
                 String preamble = "window.parent.postMessage(JSON.stringify(";
                 int start = jsonp.indexOf(preamble);
    diff --git a/core/src/main/java/hudson/model/EnvironmentContributingAction.java b/core/src/main/java/hudson/model/EnvironmentContributingAction.java
    index 761ed8ae971ffe85e71b5c153c19bd66826ac184..03ad23fb351ac065a9843183f08a64fe7e6cad13 100644
    --- a/core/src/main/java/hudson/model/EnvironmentContributingAction.java
    +++ b/core/src/main/java/hudson/model/EnvironmentContributingAction.java
    @@ -24,9 +24,14 @@
     package hudson.model;
     
     import hudson.EnvVars;
    +import hudson.Util;
     import hudson.model.Queue.Task;
     import hudson.tasks.Builder;
     import hudson.tasks.BuildWrapper;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.ProtectedExternally;
    +
    +import javax.annotation.Nonnull;
     
     /**
      * {@link Action} that contributes environment variables during a build.
    @@ -40,17 +45,43 @@ import hudson.tasks.BuildWrapper;
      *
      * @author Kohsuke Kawaguchi
      * @since 1.318
    - * @see AbstractBuild#getEnvironment(TaskListener)
    + * @see Run#getEnvironment(TaskListener)
      * @see BuildWrapper
      */
     public interface EnvironmentContributingAction extends Action {
    +    /**
    +     * Called by {@link Run} to allow plugins to contribute environment variables.
    +     *
    +     * @param run
    +     *      The calling build. Never null.
    +     * @param env
    +     *      Environment variables should be added to this map.
    +     * @since 2.76
    +     */
    +    default void buildEnvironment(@Nonnull Run<?, ?> run, @Nonnull EnvVars env) {
    +        if (run instanceof AbstractBuild
    +                && Util.isOverridden(EnvironmentContributingAction.class,
    +                                     getClass(), "buildEnvVars", AbstractBuild.class, EnvVars.class)) {
    +            buildEnvVars((AbstractBuild) run, env);
    +        }
    +    }
    +
         /**
          * Called by {@link AbstractBuild} to allow plugins to contribute environment variables.
          *
    +     * @deprecated Use {@link #buildEnvironment} instead
    +     *
          * @param build
          *      The calling build. Never null.
          * @param env
          *      Environment variables should be added to this map.
          */
    -    void buildEnvVars(AbstractBuild<?, ?> build, EnvVars env);
    +    @Deprecated
    +    @Restricted(ProtectedExternally.class)
    +    default void buildEnvVars(AbstractBuild<?, ?> build, EnvVars env) {
    +        if (Util.isOverridden(EnvironmentContributingAction.class,
    +                              getClass(), "buildEnvironment", Run.class, EnvVars.class)) {
    +            buildEnvironment(build, env);
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java
    index dac98709fd92044ef9f67ba116ff17c3c2131132..5f90ad1bfcf37b9db0046b5be6f48f453052fa92 100644
    --- a/core/src/main/java/hudson/model/Executor.java
    +++ b/core/src/main/java/hudson/model/Executor.java
    @@ -31,7 +31,7 @@ import hudson.model.queue.SubTask;
     import hudson.model.queue.WorkUnit;
     import hudson.security.ACL;
     import hudson.util.InterceptingProxy;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import jenkins.model.CauseOfInterruption;
     import jenkins.model.CauseOfInterruption.UserInterruption;
     import jenkins.model.InterruptedBuildAction;
    @@ -63,6 +63,7 @@ import java.util.logging.Logger;
     
     import static hudson.model.queue.Executables.*;
     import hudson.security.ACLContext;
    +import hudson.security.AccessControlled;
     import java.util.Collection;
     import static java.util.logging.Level.*;
     import javax.annotation.CheckForNull;
    @@ -71,6 +72,7 @@ import jenkins.model.queue.AsynchronousExecution;
     import jenkins.security.QueueItemAuthenticatorConfiguration;
     import jenkins.security.QueueItemAuthenticatorDescriptor;
     import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.DoNotUse;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     
    @@ -85,6 +87,7 @@ public class Executor extends Thread implements ModelObject {
         protected final @Nonnull Computer owner;
         private final Queue queue;
         private final ReadWriteLock lock = new ReentrantReadWriteLock();
    +    private static final int DEFAULT_ESTIMATED_DURATION = -1;
     
         @GuardedBy("lock")
         private long startTime;
    @@ -103,6 +106,11 @@ public class Executor extends Thread implements ModelObject {
         @GuardedBy("lock")
         private Queue.Executable executable;
     
    +    /**
    +     * Calculation of estimated duration needs some time, so, it's better to cache it once executable is known
    +     */
    +    private long executableEstimatedDuration = DEFAULT_ESTIMATED_DURATION;
    +
         /**
          * Used to mark that the execution is continuing asynchronously even though {@link Executor} as {@link Thread}
          * has finished.
    @@ -136,7 +144,7 @@ public class Executor extends Thread implements ModelObject {
         public Executor(@Nonnull Computer owner, int n) {
             super("Executor #"+n+" for "+owner.getDisplayName());
             this.owner = owner;
    -        this.queue = Jenkins.getInstance().getQueue();
    +        this.queue = Jenkins.get().getQueue();
             this.number = n;
         }
     
    @@ -355,6 +363,10 @@ public class Executor extends Thread implements ModelObject {
                             LOGGER.log(FINE, getName()+" grabbed "+workUnit+" from queue");
                         SubTask task = workUnit.work;
                         Executable executable = task.createExecutable();
    +                    if (executable == null) {
    +                        String displayName = task instanceof Queue.Task ? ((Queue.Task) task).getFullDisplayName() : task.getDisplayName();
    +                        LOGGER.log(WARNING, "{0} cannot be run (for example because it is disabled)", displayName);
    +                    }
                         lock.writeLock().lock();
                         try {
                             Executor.this.executable = executable;
    @@ -387,9 +399,11 @@ public class Executor extends Thread implements ModelObject {
                     // by tasks. In such case Jenkins starts the workUnit in order
                     // to report results to console outputs.
                     if (executable == null) {
    -                    throw new Error("The null Executable has been created for "+workUnit+". The task cannot be executed");
    +                    return;
                     }
     
    +                executableEstimatedDuration = executable.getEstimatedDuration();
    +
                     if (executable instanceof Actionable) {
                         if (LOGGER.isLoggable(Level.FINER)) {
                             LOGGER.log(FINER, "when running {0} from {1} we are copying {2} actions whereas the item currently has {3}", new Object[] {executable, workUnit.context.item, workUnit.context.actions, workUnit.context.item.getAllActions()});
    @@ -417,11 +431,12 @@ public class Executor extends Thread implements ModelObject {
                 } catch (AsynchronousExecution x) {
                     lock.writeLock().lock();
                     try {
    -                    x.setExecutor(this);
    +                    x.setExecutorWithoutCompleting(this);
                         this.asynchronousExecution = x;
                     } finally {
                         lock.writeLock().unlock();
                     }
    +                x.maybeComplete();
                 } catch (Throwable e) {
                     problems = e;
                 } finally {
    @@ -440,7 +455,7 @@ public class Executor extends Thread implements ModelObject {
                 LOGGER.log(FINE, getName()+" interrupted",e);
                 // die peacefully
             } catch(Exception | Error e) {
    -            LOGGER.log(SEVERE, "Unexpected executor death", e);
    +            LOGGER.log(SEVERE, getName()+": Unexpected executor death", e);
             } finally {
                 if (asynchronousExecution == null) {
                     finish2();
    @@ -475,6 +490,7 @@ public class Executor extends Thread implements ModelObject {
             if (this instanceof OneOffExecutor) {
                 owner.remove((OneOffExecutor) this);
             }
    +        executableEstimatedDuration = DEFAULT_ESTIMATED_DURATION;
             queue.scheduleMaintenance();
         }
     
    @@ -494,7 +510,6 @@ public class Executor extends Thread implements ModelObject {
          * @return
          *      null if the executor is idle.
          */
    -    @Exported
         public @CheckForNull Queue.Executable getCurrentExecutable() {
             lock.readLock().lock();
             try {
    @@ -503,6 +518,16 @@ public class Executor extends Thread implements ModelObject {
                 lock.readLock().unlock();
             }
         }
    +
    +    /**
    +     * Same as {@link #getCurrentExecutable} but checks {@link Item#READ}.
    +     */
    +    @Exported(name="currentExecutable")
    +    @Restricted(DoNotUse.class) // for exporting only
    +    public Queue.Executable getCurrentExecutableForApi() {
    +        Executable candidate = getCurrentExecutable();
    +        return candidate instanceof AccessControlled && ((AccessControlled) candidate).hasPermission(Item.READ) ? candidate : null;
    +    }
         
         /**
          * Returns causes of interruption.
    @@ -521,7 +546,6 @@ public class Executor extends Thread implements ModelObject {
          * @return
          *      null if the executor is idle.
          */
    -    @Exported
         @CheckForNull
         public WorkUnit getCurrentWorkUnit() {
             lock.readLock().lock();
    @@ -554,7 +578,7 @@ public class Executor extends Thread implements ModelObject {
         }
     
         /**
    -     * Same as {@link #getName()}.
    +     * Human readable name of the Jenkins executor. For the Java thread name use {@link #getName()}.
          */
         public String getDisplayName() {
             return "Executor #"+getNumber();
    @@ -672,18 +696,9 @@ public class Executor extends Thread implements ModelObject {
          */
         @Exported
         public int getProgress() {
    -        long d;
    -        lock.readLock().lock();
    -        try {
    -            if (executable == null) {
    -                return -1;
    -            }
    -            d = executable.getEstimatedDuration();
    -        } finally {
    -            lock.readLock().unlock();
    -        }
    +        long d = executableEstimatedDuration;
             if (d <= 0) {
    -            return -1;
    +            return DEFAULT_ESTIMATED_DURATION;
             }
     
             int num = (int) (getElapsedTime() * 100 / d);
    @@ -702,25 +717,23 @@ public class Executor extends Thread implements ModelObject {
          */
         @Exported
         public boolean isLikelyStuck() {
    -        long d;
    -        long elapsed;
             lock.readLock().lock();
             try {
                 if (executable == null) {
                     return false;
                 }
    -
    -            elapsed = getElapsedTime();
    -            d = executable.getEstimatedDuration();
             } finally {
                 lock.readLock().unlock();
             }
    +
    +        long elapsed = getElapsedTime();
    +        long d = executableEstimatedDuration;
             if (d >= 0) {
                 // if it's taking 10 times longer than ETA, consider it stuck
                 return d * 10 < elapsed;
             } else {
                 // if no ETA is available, a build taking longer than a day is considered stuck
    -            return TimeUnit2.MILLISECONDS.toHours(elapsed) > 24;
    +            return TimeUnit.MILLISECONDS.toHours(elapsed) > 24;
             }
         }
     
    @@ -762,17 +775,7 @@ public class Executor extends Thread implements ModelObject {
          * until the build completes.
          */
         public String getEstimatedRemainingTime() {
    -        long d;
    -        lock.readLock().lock();
    -        try {
    -            if (executable == null) {
    -                return Messages.Executor_NotAvailable();
    -            }
    -
    -            d = executable.getEstimatedDuration();
    -        } finally {
    -            lock.readLock().unlock();
    -        }
    +        long d = executableEstimatedDuration;
             if (d < 0) {
                 return Messages.Executor_NotAvailable();
             }
    @@ -790,24 +793,14 @@ public class Executor extends Thread implements ModelObject {
          * it as a number of milli-seconds.
          */
         public long getEstimatedRemainingTimeMillis() {
    -        long d;
    -        lock.readLock().lock();
    -        try {
    -            if (executable == null) {
    -                return -1;
    -            }
    -
    -            d = executable.getEstimatedDuration();
    -        } finally {
    -            lock.readLock().unlock();
    -        }
    +        long d = executableEstimatedDuration;
             if (d < 0) {
    -            return -1;
    +            return DEFAULT_ESTIMATED_DURATION;
             }
     
             long eta = d - getElapsedTime();
             if (eta <= 0) {
    -            return -1;
    +            return DEFAULT_ESTIMATED_DURATION;
             }
     
             return eta;
    @@ -892,16 +885,10 @@ public class Executor extends Thread implements ModelObject {
          * Returns when this executor started or should start being idle.
          */
         public long getIdleStartMilliseconds() {
    -        lock.readLock().lock();
    -        try {
    -            if (isIdle())
    -                return Math.max(creationTime, owner.getConnectTime());
    -            else {
    -                return Math.max(startTime + Math.max(0, executable == null ? -1 : executable.getEstimatedDuration()),
    -                        System.currentTimeMillis() + 15000);
    -            }
    -        } finally {
    -            lock.readLock().unlock();
    +        if (isIdle())
    +            return Math.max(creationTime, owner.getConnectTime());
    +        else {
    +            return Math.max(startTime + Math.max(0, executableEstimatedDuration), System.currentTimeMillis() + 15000);
             }
         }
     
    @@ -967,7 +954,7 @@ public class Executor extends Thread implements ModelObject {
          */
         @Deprecated
         public static long getEstimatedDurationFor(Executable e) {
    -        return e == null ? -1 : e.getEstimatedDuration();
    +        return e == null ? DEFAULT_ESTIMATED_DURATION : e.getEstimatedDuration();
         }
     
         /**
    diff --git a/core/src/main/java/hudson/model/FileParameterValue.java b/core/src/main/java/hudson/model/FileParameterValue.java
    index 85bfb043874998fc2e743a8019a4a062e186e599..f911927285ae55af04f93d234a7d2e709de54a6f 100644
    --- a/core/src/main/java/hudson/model/FileParameterValue.java
    +++ b/core/src/main/java/hudson/model/FileParameterValue.java
    @@ -36,6 +36,7 @@ import java.io.OutputStream;
     import java.io.UnsupportedEncodingException;
     import java.nio.file.Files;
     import java.nio.file.InvalidPathException;
    +import java.nio.file.Path;
     import javax.servlet.ServletException;
     
     import org.apache.commons.fileupload.FileItem;
    @@ -45,6 +46,8 @@ import org.apache.commons.fileupload.util.FileItemHeadersImpl;
     import org.apache.commons.io.FilenameUtils;
     import org.apache.commons.io.IOUtils;
     import org.apache.commons.lang.StringUtils;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.DataBoundConstructor;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
    @@ -61,6 +64,16 @@ import org.kohsuke.stapler.StaplerResponse;
      * @author Kohsuke Kawaguchi
      */
     public class FileParameterValue extends ParameterValue {
    +    private static final String FOLDER_NAME = "fileParameters";
    +
    +    /**
    +     * Escape hatch for SECURITY-1074, fileParameter used to escape their expected folder.
    +     * It's not recommended to enable for security reasons. That option is only present for backward compatibility.
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE = 
    +            Boolean.getBoolean(FileParameterValue.class.getName() + ".allowFolderTraversalOutsideWorkspace");
    +
         private transient final FileItem file;
     
         /**
    @@ -70,6 +83,9 @@ public class FileParameterValue extends ParameterValue {
     
         /**
          * Overrides the location in the build to place this file. Initially set to {@link #getName()}
    +     * The location could be directly the filename or also a hierarchical path. 
    +     * The intermediate folders will be created automatically.
    +     * Take care that no escape from the current directory is allowed and will result in the failure of the build.
          */
         private String location;
     
    @@ -142,7 +158,16 @@ public class FileParameterValue extends ParameterValue {
                 public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
                 	if (!StringUtils.isEmpty(location) && !StringUtils.isEmpty(file.getName())) {
                 	    listener.getLogger().println("Copying file to "+location);
    -                    FilePath locationFilePath = build.getWorkspace().child(location);
    +                    FilePath ws = build.getWorkspace();
    +                    if (ws == null) {
    +                        throw new IllegalStateException("The workspace should be created when setUp method is called");
    +                    }
    +                    if (!ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE && !ws.isDescendant(location)) {
    +                        listener.error("Rejecting file path escaping base directory with relative path: " + location);
    +                        // force the build to fail
    +                        return null;
    +                    }
    +                    FilePath locationFilePath = ws.child(location);
                         locationFilePath.getParent().mkdirs();
                 	    locationFilePath.copyFrom(file);
                         locationFilePath.copyTo(new FilePath(getLocationUnderBuild(build)));
    @@ -204,6 +229,18 @@ public class FileParameterValue extends ParameterValue {
             if (("/" + originalFileName).equals(request.getRestOfPath())) {
                 AbstractBuild build = (AbstractBuild)request.findAncestor(AbstractBuild.class).getObject();
                 File fileParameter = getLocationUnderBuild(build);
    +
    +            if (!ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE) {
    +                File fileParameterFolder = getFileParameterFolderUnderBuild(build);
    +
    +                //TODO can be replaced by Util#isDescendant in 2.80+
    +                Path child = fileParameter.getAbsoluteFile().toPath().normalize();
    +                Path parent = fileParameterFolder.getAbsoluteFile().toPath().normalize();
    +                if (!child.startsWith(parent)) {
    +                    throw new IllegalStateException("The fileParameter tried to escape the expected folder: " + location);
    +                }
    +            }
    +
                 if (fileParameter.isFile()) {
                     try (InputStream data = Files.newInputStream(fileParameter.toPath())) {
                         long lastModified = fileParameter.lastModified();
    @@ -227,7 +264,11 @@ public class FileParameterValue extends ParameterValue {
          * @return the location to store the file parameter
          */
         private File getLocationUnderBuild(AbstractBuild build) {
    -        return new File(build.getRootDir(), "fileParameters/" + location);
    +        return new File(getFileParameterFolderUnderBuild(build), location);
    +    }
    +
    +    private File getFileParameterFolderUnderBuild(AbstractBuild<?, ?> build){
    +        return new File(build.getRootDir(), FOLDER_NAME);
         }
     
         /**
    diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java
    index 2e33bdf2d070c85e6fb6a6f12ff0abc9aaa7be2e..e94790857676e28cca1c6def7f4b0d7f1ad30ca3 100644
    --- a/core/src/main/java/hudson/model/Fingerprint.java
    +++ b/core/src/main/java/hudson/model/Fingerprint.java
    @@ -822,24 +822,22 @@ public class Fingerprint implements ModelObject, Saveable {
         public static final class ProjectRenameListener extends ItemListener {
             @Override
             public void onLocationChanged(final Item item, final String oldName, final String newName) {
    -            try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
    +            try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
                     locationChanged(item, oldName, newName);
                 }
             }
             private void locationChanged(Item item, String oldName, String newName) {
    -            if (item instanceof AbstractProject) {
    -                AbstractProject p = Jenkins.getInstance().getItemByFullName(newName, AbstractProject.class);
    +            if (item instanceof Job) {
    +                Job p = Jenkins.getInstance().getItemByFullName(newName, Job.class);
                     if (p != null) {
    -                    RunList builds = p.getBuilds();
    -                    for (Object build : builds) {
    -                        if (build instanceof AbstractBuild) {
    -                            Collection<Fingerprint> fingerprints = ((AbstractBuild)build).getBuildFingerprints();
    -                            for (Fingerprint f : fingerprints) {
    -                                try {
    -                                    f.rename(oldName, newName);
    -                                } catch (IOException e) {
    -                                    logger.log(Level.WARNING, "Failed to update fingerprint record " + f.getFileName() + " when " + oldName + " was renamed to " + newName, e);
    -                                }
    +                    RunList<? extends Run> builds = p.getBuilds();
    +                    for (Run build : builds) {
    +                        Collection<Fingerprint> fingerprints = build.getBuildFingerprints();
    +                        for (Fingerprint f : fingerprints) {
    +                            try {
    +                                f.rename(oldName, newName);
    +                            } catch (IOException e) {
    +                                logger.log(Level.WARNING, "Failed to update fingerprint record " + f.getFileName() + " when " + oldName + " was renamed to " + newName, e);
                                 }
                             }
                         }
    @@ -1256,7 +1254,7 @@ public class Fingerprint implements ModelObject, Saveable {
                 AtomicFileWriter afw = new AtomicFileWriter(file);
                 try {
                     PrintWriter w = new PrintWriter(afw);
    -                w.println("<?xml version='1.0' encoding='UTF-8'?>");
    +                w.println("<?xml version='1.1' encoding='UTF-8'?>");
                     w.println("<fingerprint>");
                     w.print("  <timestamp>");
                     w.print(DATE_CONVERTER.toString(timestamp));
    @@ -1366,7 +1364,12 @@ public class Fingerprint implements ModelObject, Saveable {
                 start = System.currentTimeMillis();
     
             try {
    -            Fingerprint f = (Fingerprint) configFile.read();
    +            Object loaded = configFile.read();
    +            if (!(loaded instanceof Fingerprint)) {
    +                throw new IOException("Unexpected Fingerprint type. Expected " + Fingerprint.class + " or subclass but got "
    +                        + (loaded != null ? loaded.getClass() : "null"));
    +            }
    +            Fingerprint f = (Fingerprint) loaded;
                 if(logger.isLoggable(Level.FINE))
                     logger.fine("Loading fingerprint "+file+" took "+(System.currentTimeMillis()-start)+"ms");
                 if (f.facets==null)
    @@ -1435,7 +1438,7 @@ public class Fingerprint implements ModelObject, Saveable {
             // Probably it failed due to the missing Item.DISCOVER
             // We try to retrieve the job using SYSTEM user and to check permissions manually.
             final Authentication userAuth = Jenkins.getAuthentication();
    -        try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
    +        try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
                 final Item itemBySystemUser = jenkins.getItemByFullName(fullName);
                 if (itemBySystemUser == null) {
                     return false;
    @@ -1443,14 +1446,14 @@ public class Fingerprint implements ModelObject, Saveable {
     
                 // To get the item existence fact, a user needs Item.DISCOVER for the item
                 // and Item.READ for all container folders.
    -            boolean canDiscoverTheItem = itemBySystemUser.getACL().hasPermission(userAuth, Item.DISCOVER);
    +            boolean canDiscoverTheItem = itemBySystemUser.hasPermission(userAuth, Item.DISCOVER);
                 if (canDiscoverTheItem) {
                     ItemGroup<?> current = itemBySystemUser.getParent();
                     do {
                         if (current instanceof Item) {
                             final Item i = (Item) current;
                             current = i.getParent();
    -                        if (!i.getACL().hasPermission(userAuth, Item.READ)) {
    +                        if (!i.hasPermission(userAuth, Item.READ)) {
                                 canDiscoverTheItem = false;
                             }
                         } else {
    diff --git a/core/src/main/java/hudson/model/FingerprintCleanupThread.java b/core/src/main/java/hudson/model/FingerprintCleanupThread.java
    index 59187a3a400630e63968c886541652f8c439e7b4..292248ed528336439dc96c1cef7306b9881cd6ff 100644
    --- a/core/src/main/java/hudson/model/FingerprintCleanupThread.java
    +++ b/core/src/main/java/hudson/model/FingerprintCleanupThread.java
    @@ -28,9 +28,10 @@ import hudson.ExtensionList;
     import hudson.Functions;
     import jenkins.model.Jenkins;
     import org.jenkinsci.Symbol;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     import java.io.File;
    -import java.io.FileFilter;
     import java.io.IOException;
     import java.util.regex.Pattern;
     
    @@ -45,7 +46,11 @@ import java.util.regex.Pattern;
      * @author Kohsuke Kawaguchi
      */
     @Extension @Symbol("fingerprintCleanup")
    -public final class FingerprintCleanupThread extends AsyncPeriodicWork {
    +@Restricted(NoExternalUse.class)
    +public class FingerprintCleanupThread extends AsyncPeriodicWork {
    +
    +    static final String FINGERPRINTS_DIR_NAME = "fingerprints";
    +    private static final Pattern FINGERPRINT_FILE_PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml");
     
         public FingerprintCleanupThread() {
             super("Fingerprint cleanup");
    @@ -66,13 +71,13 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork {
         public void execute(TaskListener listener) {
             int numFiles = 0;
     
    -        File root = new File(Jenkins.getInstance().getRootDir(),"fingerprints");
    -        File[] files1 = root.listFiles(LENGTH2DIR_FILTER);
    +        File root = new File(getRootDir(), FINGERPRINTS_DIR_NAME);
    +        File[] files1 = root.listFiles(f -> f.isDirectory() && f.getName().length()==2);
             if(files1!=null) {
                 for (File file1 : files1) {
    -                File[] files2 = file1.listFiles(LENGTH2DIR_FILTER);
    +                File[] files2 = file1.listFiles(f -> f.isDirectory() && f.getName().length()==2);
                     for(File file2 : files2) {
    -                    File[] files3 = file2.listFiles(FINGERPRINTFILE_FILTER);
    +                    File[] files3 = file2.listFiles(f -> f.isFile() && FINGERPRINT_FILE_PATTERN.matcher(f.getName()).matches());
                         for(File file3 : files3) {
                             if(check(file3, listener))
                                 numFiles++;
    @@ -101,7 +106,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork {
          */
         private boolean check(File fingerprintFile, TaskListener listener) {
             try {
    -            Fingerprint fp = Fingerprint.load(fingerprintFile);
    +            Fingerprint fp = loadFingerprint(fingerprintFile);
                 if (fp == null || !fp.isAlive()) {
                     listener.getLogger().println("deleting obsolete " + fingerprintFile);
                     fingerprintFile.delete();
    @@ -109,8 +114,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork {
                 } else {
                     // get the fingerprint in the official map so have the changes visible to Jenkins
                     // otherwise the mutation made in FingerprintMap can override our trimming.
    -                listener.getLogger().println("possibly trimming " + fingerprintFile);
    -                fp = Jenkins.getInstance()._getFingerprint(fp.getHashString());
    +                fp = getFingerprint(fp);
                     return fp.trim();
                 }
             } catch (IOException e) {
    @@ -119,17 +123,16 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork {
             }
         }
     
    -    private static final FileFilter LENGTH2DIR_FILTER = new FileFilter() {
    -        public boolean accept(File f) {
    -            return f.isDirectory() && f.getName().length()==2;
    -        }
    -    };
    +    protected Fingerprint loadFingerprint(File fingerprintFile) throws IOException {
    +        return Fingerprint.load(fingerprintFile);
    +    }
     
    -    private static final FileFilter FINGERPRINTFILE_FILTER = new FileFilter() {
    -        private final Pattern PATTERN = Pattern.compile("[0-9a-f]{28}\\.xml");
    +    protected Fingerprint getFingerprint(Fingerprint fp) throws IOException {
    +        return Jenkins.get()._getFingerprint(fp.getHashString());
    +    }
    +
    +    protected File getRootDir() {
    +        return Jenkins.get().getRootDir();
    +    }
     
    -        public boolean accept(File f) {
    -            return f.isFile() && PATTERN.matcher(f.getName()).matches();
    -        }
    -    };
     }
    diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java
    index db063b952c20112acc5c2b3f0beb7bc04dafba41..cf72f3b3366fa62e5d0436a857ec2f24d8765852 100644
    --- a/core/src/main/java/hudson/model/Hudson.java
    +++ b/core/src/main/java/hudson/model/Hudson.java
    @@ -34,7 +34,6 @@ import hudson.model.listeners.ItemListener;
     import hudson.slaves.ComputerListener;
     import hudson.util.CopyOnWriteList;
     import hudson.util.FormValidation;
    -import javax.annotation.Nonnull;
     import jenkins.model.Jenkins;
     import org.jvnet.hudson.reactor.ReactorException;
     import org.kohsuke.stapler.QueryParameter;
    @@ -52,7 +51,7 @@ import java.text.ParseException;
     import java.util.List;
     
     import static hudson.Util.fixEmpty;
    -import javax.annotation.CheckForNull;
    +import javax.annotation.Nullable;
     
     public class Hudson extends Jenkins {
     
    @@ -70,10 +69,10 @@ public class Hudson extends Jenkins {
         @Deprecated
         private transient final CopyOnWriteList<ComputerListener> computerListeners = ExtensionListView.createCopyOnWriteList(ComputerListener.class);
     
    -    /** @deprecated Here only for compatibility. Use {@link Jenkins#getInstance} instead. */
    +    /** @deprecated Here only for compatibility. Use {@link Jenkins#get} instead. */
         @Deprecated
         @CLIResolver
    -    @Nonnull
    +    @Nullable
         public static Hudson getInstance() {
             return (Hudson)Jenkins.getInstance();
         }
    diff --git a/core/src/main/java/hudson/model/InvisibleAction.java b/core/src/main/java/hudson/model/InvisibleAction.java
    index 25242c332afb358e663522527d01ddda9e9922c8..bd0ef82b522b6d49dca9f6cdc616187c4518da99 100644
    --- a/core/src/main/java/hudson/model/InvisibleAction.java
    +++ b/core/src/main/java/hudson/model/InvisibleAction.java
    @@ -24,12 +24,16 @@
     package hudson.model;
     
     /**
    - * Partial {@link Action} implementation that doesn't have any UI presence.
    + * Partial {@link Action} implementation that doesn't have any UI presence (unless the {@link #getUrlName()} is overrided).
      *
      * <p>
      * This class can be used as a convenient base class, when you use
      * {@link Action} for just storing data associated with a build.
      *
    + * <p>
    + * It could also be used to reduce the amount of code required to just create an accessible url for tests 
    + * by overriding the {@link #getUrlName()} method.
    + *
      * @author Kohsuke Kawaguchi
      * @since 1.188
      */
    @@ -42,7 +46,7 @@ public abstract class InvisibleAction implements Action {
             return null;
         }
     
    -    public final String getUrlName() {
    +    public String getUrlName() {
             return null;
         }
     }
    diff --git a/core/src/main/java/hudson/model/Item.java b/core/src/main/java/hudson/model/Item.java
    index d0c438744d86899d6b7c28b8124f1778c400d9c5..be8a5495e16d886a8d543f4caf9f0951955bb05e 100644
    --- a/core/src/main/java/hudson/model/Item.java
    +++ b/core/src/main/java/hudson/model/Item.java
    @@ -25,9 +25,12 @@
     package hudson.model;
     
     import hudson.Functions;
    +import hudson.Util;
    +import jenkins.model.Jenkins;
     import jenkins.util.SystemProperties;
     import hudson.security.PermissionScope;
     import jenkins.util.io.OnMaster;
    +import jline.internal.Nullable;
     import org.kohsuke.stapler.StaplerRequest;
     
     import java.io.IOException;
    @@ -39,6 +42,9 @@ import hudson.security.PermissionGroup;
     import hudson.security.AccessControlled;
     import hudson.util.Secret;
     
    +import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
    +
     /**
      * Basic configuration unit in Hudson.
      *
    @@ -131,18 +137,32 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
         /**
          * Gets the relative name to this item from the specified group.
          *
    +     * @param g
    +     *      The {@link ItemGroup} instance used as context to evaluate the relative name of this item
    +     * @return
    +     *      The name of the current item, relative to p. Nested {@link ItemGroup}s are separated by {@code /} character.
          * @since 1.419
          * @return
    -     *      String like "../foo/bar"
    +     *      String like "../foo/bar".
    +     *      {@code null} if one of item parents is not an {@link Item}.
          */
    -    String getRelativeNameFrom(ItemGroup g);
    +    @Nullable
    +    default String getRelativeNameFrom(@CheckForNull ItemGroup g) {
    +        return Functions.getRelativeNameFrom(this, g);
    +    }
     
         /**
          * Short for {@code getRelativeNameFrom(item.getParent())}
          *
    +     * @return String like "../foo/bar".
    +     *      {@code null} if one of item parents is not an {@link Item}.
          * @since 1.419
          */
    -    String getRelativeNameFrom(Item item);
    +    @Nullable
    +    default String getRelativeNameFrom(@Nonnull Item item)  {
    +        return getRelativeNameFrom(item.getParent());
    +
    +    }
     
         /**
          * Returns the URL of this item relative to the context root of the application.
    @@ -180,7 +200,12 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
          *      (even this won't work for the same reason, which should be fixed.)
          */
         @Deprecated
    -    String getAbsoluteUrl();
    +    default String getAbsoluteUrl() {
    +        String r = Jenkins.getInstance().getRootUrl();
    +        if(r==null)
    +            throw new IllegalStateException("Root URL isn't configured yet. Cannot compute absolute URL.");
    +        return Util.encode(r+getUrl());
    +    }
     
         /**
          * Called right after when a {@link Item} is loaded from disk.
    @@ -207,7 +232,9 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
          *
          * @since 1.374
           */
    -    default void onCreatedFromScratch() {}
    +    default void onCreatedFromScratch() {
    +        // do nothing by default
    +    }
     
         /**
          * Save the settings to a file.
    @@ -239,5 +266,5 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
         Permission BUILD = new Permission(PERMISSIONS, "Build", Messages._AbstractProject_BuildPermission_Description(),  Permission.UPDATE, PermissionScope.ITEM);
         Permission WORKSPACE = new Permission(PERMISSIONS, "Workspace", Messages._AbstractProject_WorkspacePermission_Description(), Permission.READ, PermissionScope.ITEM);
         Permission WIPEOUT = new Permission(PERMISSIONS, "WipeOut", Messages._AbstractProject_WipeOutPermission_Description(), null, Functions.isWipeOutPermissionEnabled(), new PermissionScope[]{PermissionScope.ITEM});
    -    Permission CANCEL = new Permission(PERMISSIONS, "Cancel", Messages._AbstractProject_CancelPermission_Description(), BUILD, PermissionScope.ITEM);
    +    Permission CANCEL = new Permission(PERMISSIONS, "Cancel", Messages._AbstractProject_CancelPermission_Description(), Permission.UPDATE, PermissionScope.ITEM);
     }
    diff --git a/core/src/main/java/hudson/model/ItemGroup.java b/core/src/main/java/hudson/model/ItemGroup.java
    index 546c9bb712c54502c8f8a081dc2cd3bedefe9027..2397587a0b9547503a50d885553e6da5ed59e993 100644
    --- a/core/src/main/java/hudson/model/ItemGroup.java
    +++ b/core/src/main/java/hudson/model/ItemGroup.java
    @@ -27,6 +27,7 @@ import hudson.model.listeners.ItemListener;
     import java.io.IOException;
     import java.util.Collection;
     import java.io.File;
    +import java.util.List;
     import javax.annotation.CheckForNull;
     import org.acegisecurity.AccessDeniedException;
     
    @@ -88,4 +89,42 @@ public interface ItemGroup<T extends Item> extends PersistenceRoot, ModelObject
          * Internal method. Called by {@link Item}s when they are deleted by users.
          */
         void onDeleted(T item) throws IOException;
    +
    +    /**
    +     * Gets all the {@link Item}s recursively in the {@link ItemGroup} tree
    +     * and filter them by the given type.
    +     * @since 2.93
    +     */
    +    default <T extends Item> List<T> getAllItems(Class<T> type) {
    +        return Items.getAllItems(this, type);
    +    }
    +
    +    /**
    +     * Gets all the {@link Item}s unordered, lazily and recursively in the {@link ItemGroup} tree
    +     * and filter them by the given type.
    +     * @since 2.93
    +     */
    +    default <T extends Item> Iterable<T> allItems(Class<T> type) {
    +        return Items.allItems(this, type);
    +    }
    +
    +    /**
    +     * Gets all the items recursively.
    +     * @since 2.93
    +     */
    +    default List<Item> getAllItems() {
    +        return getAllItems(Item.class);
    +    }
    +
    +    /**
    +     * Gets all the items unordered, lazily and recursively.
    +     * @since 2.93
    +     */
    +    default Iterable<Item> allItems() {
    +        return allItems(Item.class);
    +    }
    +
    +    // TODO could delegate to allItems overload taking Authentication, but perhaps more useful to introduce a variant to perform preauth filtering using Predicate and check Item.READ afterwards
    +    // or return a Stream<Item> and provide a Predicate<Item> public static Items.readable(), and see https://stackoverflow.com/q/22694884/12916 if you are looking for just one result
    +
     }
    diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java
    index 13f72c3f079219432ed98748324b9ed3d6aa7d44..195d4d8c0ce95322284d279d8b6176286cd24958 100644
    --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java
    +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java
    @@ -45,6 +45,8 @@ import java.io.File;
     import java.io.FileFilter;
     import java.io.IOException;
     import java.io.InputStream;
    +import java.nio.file.Files;
    +import java.nio.file.StandardCopyOption;
     import java.util.Map;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    @@ -239,7 +241,8 @@ public abstract class ItemGroupMixIn {
             T result = (T)createProject(src.getDescriptor(),name,false);
     
             // copy config
    -        Util.copyFile(srcConfigFile.getFile(), Items.getConfigFile(result).getFile());
    +        Files.copy(Util.fileToPath(srcConfigFile.getFile()), Util.fileToPath(Items.getConfigFile(result).getFile()),
    +                StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
     
             // reload from the new config
             final File rootDir = result.getRootDir();
    diff --git a/core/src/main/java/hudson/model/Items.java b/core/src/main/java/hudson/model/Items.java
    index 927abc72e0bbe5e658d85c3eb14214b90fcbfbf6..d0a26238a3d3f71a650c631ab5ca2caec3a901b3 100644
    --- a/core/src/main/java/hudson/model/Items.java
    +++ b/core/src/main/java/hudson/model/Items.java
    @@ -35,6 +35,7 @@ import hudson.security.AccessControlled;
     import hudson.triggers.Trigger;
     import hudson.util.DescriptorList;
     import hudson.util.EditDistance;
    +import jenkins.util.MemoryReductionUtil;
     import hudson.util.XStream2;
     import java.io.File;
     import java.io.IOException;
    @@ -313,8 +314,8 @@ public class Items {
     
         // Had difficulty adapting the version in Functions to use no live items, so rewrote it:
         static String getRelativeNameFrom(String itemFullName, String groupFullName) {
    -        String[] itemFullNameA = itemFullName.isEmpty() ? new String[0] : itemFullName.split("/");
    -        String[] groupFullNameA = groupFullName.isEmpty() ? new String[0] : groupFullName.split("/");
    +        String[] itemFullNameA = itemFullName.isEmpty() ? MemoryReductionUtil.EMPTY_STRING_ARRAY : itemFullName.split("/");
    +        String[] groupFullNameA = groupFullName.isEmpty() ? MemoryReductionUtil.EMPTY_STRING_ARRAY : groupFullName.split("/");
             for (int i = 0; ; i++) {
                 if (i == itemFullNameA.length) {
                     if (i == groupFullNameA.length) {
    diff --git a/core/src/main/java/hudson/model/JDK.java b/core/src/main/java/hudson/model/JDK.java
    index b04376eb74aead7cde5dc2bac587aa49bf3a30b2..abcb1a9f74d039af0fce62acac8e52b89da2effe 100644
    --- a/core/src/main/java/hudson/model/JDK.java
    +++ b/core/src/main/java/hudson/model/JDK.java
    @@ -32,16 +32,20 @@ import hudson.EnvVars;
     import hudson.slaves.NodeSpecific;
     import hudson.tools.ToolInstallation;
     import hudson.tools.ToolDescriptor;
    +import hudson.tools.ToolInstaller;
     import hudson.tools.ToolProperty;
    -import hudson.tools.JDKInstaller;
     import hudson.util.XStream2;
     
     import java.io.File;
     import java.io.IOException;
    +import java.lang.reflect.Constructor;
    +import java.lang.reflect.InvocationTargetException;
     import java.util.Map;
     import java.util.List;
     import java.util.Arrays;
     import java.util.Collections;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
     
     import jenkins.model.Jenkins;
     import org.jenkinsci.Symbol;
    @@ -183,8 +187,18 @@ public final class JDK extends ToolInstallation implements NodeSpecific<JDK>, En
             }
     
             @Override
    -        public List<JDKInstaller> getDefaultInstallers() {
    -            return Collections.singletonList(new JDKInstaller(null,false));
    +        public List<? extends ToolInstaller> getDefaultInstallers() {
    +            try {
    +                Class<? extends ToolInstaller> jdkInstallerClass = Jenkins.getInstance().getPluginManager()
    +                        .uberClassLoader.loadClass("hudson.tools.JDKInstaller").asSubclass(ToolInstaller.class);
    +                Constructor<? extends ToolInstaller> constructor = jdkInstallerClass.getConstructor(String.class, boolean.class);
    +                return Collections.singletonList(constructor.newInstance(null, false));
    +            } catch (ClassNotFoundException e) {
    +                return Collections.emptyList();
    +            } catch (Exception e) {
    +                LOGGER.log(Level.WARNING, "Unable to get default installer", e);
    +                return Collections.emptyList();
    +            }
             }
     
             /**
    @@ -211,4 +225,6 @@ public final class JDK extends ToolInstallation implements NodeSpecific<JDK>, En
                 return ((JDK)obj).javaHome;
             }
         }
    +
    +    private static final Logger LOGGER = Logger.getLogger(JDK.class.getName());
     }
    diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java
    index 0575724d9971c9ee5624be5f15a8d6ce24b0c4bf..4b149034bd825bf448e8b6aad5416a8afd748b12 100644
    --- a/core/src/main/java/hudson/model/Job.java
    +++ b/core/src/main/java/hudson/model/Job.java
    @@ -29,6 +29,7 @@ import hudson.EnvVars;
     import hudson.Extension;
     import hudson.ExtensionPoint;
     import hudson.FeedAdapter;
    +import hudson.FilePath;
     import hudson.PermalinkList;
     import hudson.Util;
     import hudson.cli.declarative.CLIResolver;
    @@ -55,7 +56,6 @@ import hudson.util.DescribableList;
     import hudson.util.FormApply;
     import hudson.util.Graph;
     import hudson.util.ProcessTree;
    -import hudson.util.QuotedStringTokenizer;
     import hudson.util.RunList;
     import hudson.util.ShiftedCategoryAxis;
     import hudson.util.StackedAreaRenderer2;
    @@ -67,7 +67,7 @@ import java.awt.Color;
     import java.awt.Paint;
     import java.io.File;
     import java.io.IOException;
    -import java.net.URLEncoder;
    +import java.nio.file.Files;
     import java.util.ArrayList;
     import java.util.Calendar;
     import java.util.Collection;
    @@ -319,6 +319,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
         /**
          * Returns whether the name of this job can be changed by user.
          */
    +    @Override
         public boolean isNameEditable() {
             return true;
         }
    @@ -374,13 +375,15 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
          *      (in which case none of the node specific properties would be reflected in the resulting override.)
          */
         public @Nonnull EnvVars getEnvironment(@CheckForNull Node node, @Nonnull TaskListener listener) throws IOException, InterruptedException {
    -        EnvVars env;
    +        EnvVars env = new EnvVars();
     
    -        if (node!=null) {
    +        if (node != null) {
                 final Computer computer = node.toComputer();
    -            env = (computer != null) ? computer.buildEnvironment(listener) : new EnvVars();                
    -        } else {
    -            env = new EnvVars();
    +            if (computer != null) {
    +                // we need to get computer environment to inherit platform details 
    +                env = computer.getEnvironment();
    +                env.putAll(computer.buildEnvironment(listener));
    +            }
             }
     
             env.putAll(getCharacteristicEnvVars());
    @@ -675,6 +678,40 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
             Util.deleteRecursive(getBuildDir());
         }
     
    +    @Restricted(NoExternalUse.class)
    +    @Extension
    +    public static class SubItemBuildsLocationImpl extends ItemListener {
    +        @Override
    +        public void onLocationChanged(Item item, String oldFullName, String newFullName) {
    +            final Jenkins jenkins = Jenkins.getInstance();
    +            if (!jenkins.isDefaultBuildDir() && item instanceof Job) {
    +                File newBuildDir = ((Job)item).getBuildDir();
    +                try {
    +                    if (!Util.isDescendant(item.getRootDir(), newBuildDir)) {
    +                        //OK builds are stored somewhere outside of the item's root, so none of the other move operations has probably moved it.
    +                        //So let's try even though we lack some information
    +                        String oldBuildsDir = Jenkins.expandVariablesForDirectory(jenkins.getRawBuildsDir(), oldFullName, "<NOPE>");
    +                        if (oldBuildsDir.contains("<NOPE>")) {
    +                            LOGGER.severe(String.format("Builds directory for job %1$s appears to be outside of item root," +
    +                                    " but somehow still containing the item root path, which is unknown. Cannot move builds from %2$s to %1$s.", newFullName, oldFullName));
    +                        } else {
    +                            File oldDir = new File(oldBuildsDir);
    +                            if (oldDir.isDirectory()) {
    +                                try {
    +                                    FileUtils.moveDirectory(oldDir, newBuildDir);
    +                                } catch (IOException e) {
    +                                    LOGGER.log(Level.SEVERE, String.format("Failed to move %s to %s", oldBuildsDir, newBuildDir.getAbsolutePath()), e);
    +                                }
    +                            }
    +                        }
    +                    }
    +                } catch (IOException e) {
    +                    LOGGER.log(Level.WARNING, "Failed to inspect " + item.getRootDir() + ". Builds might not be moved.", e);
    +                }
    +            }
    +        }
    +    }
    +
         /**
          * Returns true if we should display "build now" icon
          */
    @@ -774,7 +811,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
         }
     
         /**
    -     * Gets the youngest build #m that satisfies <tt>n&lt;=m</tt>.
    +     * Gets the youngest build #m that satisfies {@code n&lt;=m}.
          * 
          * This is useful when you'd like to fetch a build but the exact build might
          * be already gone (deleted, rotated, etc.)
    @@ -789,7 +826,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
         }
     
         /**
    -     * Gets the latest build #m that satisfies <tt>m&lt;=n</tt>.
    +     * Gets the latest build #m that satisfies {@code m&lt;=n}.
          * 
          * This is useful when you'd like to fetch a build but the exact build might
          * be already gone (deleted, rotated, etc.)
    @@ -1051,7 +1088,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
         /**
          * RSS feed for changes in this project.
          *
    -     * @since TODO
    +     * @since 2.60
          */
         public void doRssChangelog(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
             class FeedItem {
    @@ -1315,39 +1352,17 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
                 }
                 ItemListener.fireOnUpdated(this);
     
    -            String newName = req.getParameter("name");
                 final ProjectNamingStrategy namingStrategy = Jenkins.getInstance().getProjectNamingStrategy();
    -            if (validRename(name, newName)) {
    -                newName = newName.trim();
    -                // check this error early to avoid HTTP response splitting.
    -                Jenkins.checkGoodName(newName);
    -                namingStrategy.checkName(newName);
    -                if (FormApply.isApply(req)) {
    -                    FormApply.applyResponse("notificationBar.show(" + QuotedStringTokenizer.quote(Messages.Job_you_must_use_the_save_button_if_you_wish()) + ",notificationBar.WARNING)").generateResponse(req, rsp, null);
    -                } else {
    -                    rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8"));
    -                }
    -            } else {
                     if(namingStrategy.isForceExistingJobs()){
                         namingStrategy.checkName(name);
                     }
                     FormApply.success(".").generateResponse(req, rsp, null);
    -            }
             } catch (JSONException e) {
                 LOGGER.log(Level.WARNING, "failed to parse " + json, e);
                 sendError(e, req, rsp);
             }
         }
     
    -    private boolean validRename(String oldName, String newName) {
    -        if (newName == null) {
    -            return false;
    -        }
    -        boolean noChange = oldName.equals(newName);
    -        boolean spaceAdded = oldName.equals(newName.trim());
    -        return !noChange && !spaceAdded;
    -    }
    -
         /**
          * Derived class can override this to perform additional config submission
          * work.
    @@ -1545,32 +1560,25 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
     
         /**
          * Renames this job.
    +     * @deprecated Exists for backwards compatibility, use {@link #doConfirmRename} instead.
          */
    +    @Deprecated
         @RequirePOST
         public/* not synchronized. see renameTo() */void doDoRename(
                 StaplerRequest req, StaplerResponse rsp) throws IOException,
                 ServletException {
    -
    -        if (!hasPermission(CONFIGURE)) {
    -            // rename is essentially delete followed by a create
    -            checkPermission(CREATE);
    -            checkPermission(DELETE);
    -        }
    -
             String newName = req.getParameter("newName");
    -        Jenkins.checkGoodName(newName);
    +        doConfirmRename(newName).generateResponse(req, rsp, null);
    +    }
     
    +    /**
    +     * {@inheritDoc}
    +     */
    +    @Override
    +    protected void checkRename(String newName) throws Failure {
             if (isBuilding()) {
    -            // redirect to page explaining that we can't rename now
    -            rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8"));
    -            return;
    +            throw new Failure(Messages.Job_NoRenameWhileBuilding());
             }
    -
    -        renameTo(newName);
    -        // send to the new job page
    -        // note we can't use getUrl() because that would pick up old name in the
    -        // Ancestor.getUrl()
    -        rsp.sendRedirect2("../" + newName);
         }
     
         public void doRssAll(StaplerRequest req, StaplerResponse rsp)
    diff --git a/core/src/main/java/hudson/model/JobProperty.java b/core/src/main/java/hudson/model/JobProperty.java
    index 2c982286e41a57b62344d93d10f7e8c3b07ad46a..a16c685ec73a000018e0d48d51f6e132abe017c7 100644
    --- a/core/src/main/java/hudson/model/JobProperty.java
    +++ b/core/src/main/java/hudson/model/JobProperty.java
    @@ -53,9 +53,9 @@ import javax.annotation.Nonnull;
      * configuration screen, and they are persisted with the job object.
      *
      * <p>
    - * Configuration screen should be defined in <tt>config.jelly</tt>.
    + * Configuration screen should be defined in {@code config.jelly}.
      * Within this page, the {@link JobProperty} instance is available
    - * as <tt>instance</tt> variable (while <tt>it</tt> refers to {@link Job}.
    + * as {@code instance} variable (while {@code it} refers to {@link Job}.
      *
      * <p>
      * Starting 1.150, {@link JobProperty} implements {@link BuildStep},
    diff --git a/core/src/main/java/hudson/model/Label.java b/core/src/main/java/hudson/model/Label.java
    index cf6fdbff8b2f396f8ea2789071bf9a10b4a9fc79..606068104e3f3ecf925343c70973908d0183eb53 100644
    --- a/core/src/main/java/hudson/model/Label.java
    +++ b/core/src/main/java/hudson/model/Label.java
    @@ -49,6 +49,8 @@ import jenkins.model.Jenkins;
     import jenkins.model.ModelObjectWithChildren;
     import org.acegisecurity.context.SecurityContext;
     import org.acegisecurity.context.SecurityContextHolder;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.DoNotUse;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.export.Exported;
    @@ -58,6 +60,7 @@ import java.io.StringReader;
     import java.util.ArrayList;
     import java.util.Collection;
     import java.util.Collections;
    +import java.util.Comparator;
     import java.util.HashSet;
     import java.util.List;
     import java.util.Set;
    @@ -127,7 +130,7 @@ public abstract class Label extends Actionable implements Comparable<Label>, Mod
         /**
          * Alias for {@link #getDisplayName()}.
          */
    -    @Exported
    +    @Exported(visibility=2)
         public final String getName() {
             return getDisplayName();
         }
    @@ -196,6 +199,16 @@ public abstract class Label extends Actionable implements Comparable<Label>, Mod
             return nodes.size() == 1 && nodes.iterator().next().getSelfLabel() == this;
         }
     
    +    private static class NodeSorter implements Comparator<Node> {
    +        @Override
    +        public int compare(Node o1, Node o2) {
    +            if (o1 == o2) {
    +                return 0;
    +            }
    +            return o1 instanceof Jenkins ? -1 : (o2 instanceof Jenkins ? 1 : o1.getNodeName().compareTo(o2.getNodeName()));
    +        }
    +    }
    +
         /**
          * Gets all {@link Node}s that belong to this label.
          */
    @@ -204,7 +217,7 @@ public abstract class Label extends Actionable implements Comparable<Label>, Mod
             Set<Node> nodes = this.nodes;
             if(nodes!=null) return nodes;
     
    -        Set<Node> r = new HashSet<Node>();
    +        Set<Node> r = new HashSet<>();
             Jenkins h = Jenkins.getInstance();
             if(this.matches(h))
                 r.add(h);
    @@ -215,6 +228,13 @@ public abstract class Label extends Actionable implements Comparable<Label>, Mod
             return this.nodes = Collections.unmodifiableSet(r);
         }
     
    +    @Restricted(DoNotUse.class) // Jelly
    +    public Set<Node> getSortedNodes() {
    +        Set<Node> r = new TreeSet<>(new NodeSorter());
    +        r.addAll(getNodes());
    +        return r;
    +    }
    +
         /**
          * Gets all {@link Cloud}s that can launch for this label.
          */
    diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java
    index f0ce0271aa0d14b49b18dcc2ba0b72405b95ef8c..9d84eb32cebb49ea99f06ffb7f57fc1e8b688d41 100644
    --- a/core/src/main/java/hudson/model/ListView.java
    +++ b/core/src/main/java/hudson/model/ListView.java
    @@ -29,6 +29,8 @@ import hudson.Util;
     import hudson.diagnosis.OldDataMonitor;
     import hudson.model.Descriptor.FormException;
     import hudson.model.listeners.ItemListener;
    +import hudson.search.CollectionSearchIndex;
    +import hudson.search.SearchIndexBuilder;
     import hudson.security.ACL;
     import hudson.security.ACLContext;
     import hudson.util.CaseInsensitiveComparator;
    @@ -45,6 +47,7 @@ import java.util.logging.Logger;
     import java.util.regex.Pattern;
     import java.util.regex.PatternSyntaxException;
     
    +import javax.annotation.CheckForNull;
     import javax.annotation.concurrent.GuardedBy;
     import javax.servlet.ServletException;
     import jenkins.model.Jenkins;
    @@ -167,7 +170,8 @@ public class ListView extends View implements DirectlyModifiableView {
         public DescribableList<ListViewColumn, Descriptor<ListViewColumn>> getColumns() {
             return columns;
         }
    -    
    +
    +
         /**
          * Returns a read-only view of all {@link Job}s in this view.
          *
    @@ -177,6 +181,20 @@ public class ListView extends View implements DirectlyModifiableView {
          */
         @Override
         public List<TopLevelItem> getItems() {
    +        return getItems(this.recurse);
    +     }
    +
    +    /**
    +     * Returns a read-only view of all {@link Job}s in this view.
    +     *
    +     *
    +     * <p>
    +     * This method returns a separate copy each time to avoid
    +     * concurrent modification issue.
    +     * @param recurse {@code false} not to recurse in ItemGroups
    +     * true to recurse in ItemGroups
    +     */
    +    private List<TopLevelItem> getItems(boolean recurse) {
             SortedSet<String> names;
             List<TopLevelItem> items = new ArrayList<TopLevelItem>();
     
    @@ -191,7 +209,7 @@ public class ListView extends View implements DirectlyModifiableView {
             Boolean statusFilter = this.statusFilter; // capture the value to isolate us from concurrent update
             Iterable<? extends TopLevelItem> candidates;
             if (recurse) {
    -            candidates = Items.getAllItems(parent, TopLevelItem.class);
    +            candidates = parent.getAllItems(TopLevelItem.class);
             } else {
                 candidates = parent.getItems();
             }
    @@ -216,6 +234,23 @@ public class ListView extends View implements DirectlyModifiableView {
             return items;
         }
     
    +    @Override
    +    public SearchIndexBuilder makeSearchIndex() {
    +        SearchIndexBuilder sib = new SearchIndexBuilder().addAllAnnotations(this);
    +        sib.add(new CollectionSearchIndex<TopLevelItem>() {// for jobs in the view
    +            protected TopLevelItem get(String key) { return getItem(key); }
    +            protected Collection<TopLevelItem> all() { return getItems(); }
    +            @Override
    +            protected String getName(TopLevelItem o) {
    +                // return the name instead of the display for suggestion searching
    +                return o.getName();
    +            }
    +        });
    +        // add the display name for each item in the search index
    +        addDisplayNamesToSearchIndex(sib, getItems(true));
    +        return sib;
    +    }
    +
         private List<TopLevelItem> expand(Collection<TopLevelItem> items, List<TopLevelItem> allItems) {
             for (TopLevelItem item : items) {
                 if (item instanceof ItemGroup) {
    @@ -378,13 +413,16 @@ public class ListView extends View implements DirectlyModifiableView {
                 throw new Failure("Query parameter 'name' is required");
     
             TopLevelItem item = resolveName(name);
    +        if (item==null)
    +            throw new Failure("Query parameter 'name' does not correspond to a known and readable item");
    +
             if (remove(item))
                 owner.save();
     
             return HttpResponses.ok();
         }
     
    -    private TopLevelItem resolveName(String name) {
    +    private @CheckForNull TopLevelItem resolveName(String name) {
             TopLevelItem item = getOwner().getItemGroup().getItem(name);
             if (item == null) {
                 name = Items.getCanonicalName(getOwner().getItemGroup(), name);
    @@ -406,7 +444,7 @@ public class ListView extends View implements DirectlyModifiableView {
                 jobNames.clear();
                 Iterable<? extends TopLevelItem> items;
                 if (recurse) {
    -                items = Items.getAllItems(getOwner().getItemGroup(), TopLevelItem.class);
    +                items = getOwner().getItemGroup().getAllItems(TopLevelItem.class);
                 } else {
                     items = getOwner().getItemGroup().getItems();
                 }
    @@ -480,25 +518,26 @@ public class ListView extends View implements DirectlyModifiableView {
         public static final class Listener extends ItemListener {
             @Override
             public void onLocationChanged(final Item item, final String oldFullName, final String newFullName) {
    -            try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
    -                locationChanged(item, oldFullName, newFullName);
    +            try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
    +                locationChanged(oldFullName, newFullName);
                 }
             }
    -        private void locationChanged(Item item, String oldFullName, String newFullName) {
    +        private void locationChanged(String oldFullName, String newFullName) {
                 final Jenkins jenkins = Jenkins.getInstance();
    -            for (View view: jenkins.getViews()) {
    -                if (view instanceof ListView) {
    -                    renameViewItem(oldFullName, newFullName, jenkins, (ListView) view);
    -                }
    -            }
    +            locationChanged(jenkins, oldFullName, newFullName);
                 for (Item g : jenkins.allItems()) {
                     if (g instanceof ViewGroup) {
    -                    ViewGroup vg = (ViewGroup) g;
    -                    for (View v : vg.getViews()) {
    -                        if (v instanceof ListView) {
    -                            renameViewItem(oldFullName, newFullName, vg, (ListView) v);
    -                        }
    -                    }
    +                    locationChanged((ViewGroup) g, oldFullName, newFullName);
    +                }
    +            }
    +        }
    +        private void locationChanged(ViewGroup vg, String oldFullName, String newFullName) {
    +            for (View v : vg.getViews()) {
    +                if (v instanceof ListView) {
    +                    renameViewItem(oldFullName, newFullName, vg, (ListView) v);
    +                }
    +                if (v instanceof ViewGroup) {
    +                    locationChanged((ViewGroup) v, oldFullName, newFullName);
                     }
                 }
             }
    @@ -524,25 +563,26 @@ public class ListView extends View implements DirectlyModifiableView {
     
             @Override
             public void onDeleted(final Item item) {
    -            try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
    +            try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
                     deleted(item);
                 }
             }
             private void deleted(Item item) {
                 final Jenkins jenkins = Jenkins.getInstance();
    -            for (View view: jenkins.getViews()) {
    -                if (view instanceof ListView) {
    -                    deleteViewItem(item, jenkins, (ListView) view);
    -                }
    -            }
    +            deleted(jenkins, item);
                 for (Item g : jenkins.allItems()) {
                     if (g instanceof ViewGroup) {
    -                    ViewGroup vg = (ViewGroup) g;
    -                    for (View v : vg.getViews()) {
    -                        if (v instanceof ListView) {
    -                            deleteViewItem(item, vg, (ListView) v);
    -                        }
    -                    }
    +                    deleted((ViewGroup) g, item);
    +                }
    +            }
    +        }
    +        private void deleted(ViewGroup vg, Item item) {
    +            for (View v : vg.getViews()) {
    +                if (v instanceof ListView) {
    +                    deleteViewItem(item, vg, (ListView) v);
    +                }
    +                if (v instanceof ViewGroup) {
    +                    deleted((ViewGroup) v, item);
                     }
                 }
             }
    diff --git a/core/src/main/java/hudson/model/LoadBalancer.java b/core/src/main/java/hudson/model/LoadBalancer.java
    index a0403e1b974f957c45e882fc9aa190783be99c68..ce69e89779309af042019fdd4365734f977e8d7e 100644
    --- a/core/src/main/java/hudson/model/LoadBalancer.java
    +++ b/core/src/main/java/hudson/model/LoadBalancer.java
    @@ -112,7 +112,7 @@ public abstract class LoadBalancer implements ExtensionPoint {
             private boolean assignGreedily(Mapping m, Task task, List<ConsistentHash<ExecutorChunk>> hashes, int i) {
                 if (i==hashes.size())   return true;    // fully assigned
     
    -            String key = task.getFullDisplayName() + (i>0 ? String.valueOf(i) : "");
    +            String key = task.getAffinityKey() + (i>0 ? String.valueOf(i) : "");
     
                 for (ExecutorChunk ec : hashes.get(i).list(key)) {
                     // let's attempt this assignment
    diff --git a/core/src/main/java/hudson/model/ManagementLink.java b/core/src/main/java/hudson/model/ManagementLink.java
    index 041fda35d05ef0b18b8405a21701236517205813..00d6871097cc66730ad6a50caa3bb3d3bf15b67a 100644
    --- a/core/src/main/java/hudson/model/ManagementLink.java
    +++ b/core/src/main/java/hudson/model/ManagementLink.java
    @@ -37,7 +37,7 @@ import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     
     /**
    - * Extension point to add icon to <tt>http://server/hudson/manage</tt> page.
    + * Extension point to add icon to {@code http://server/hudson/manage} page.
      *
      * <p>
      * This is a place for exposing features that are only meant for system admins
    diff --git a/core/src/main/java/hudson/model/ModelObject.java b/core/src/main/java/hudson/model/ModelObject.java
    index 5467b2a7a77e6e02ec2b073148d670ce55a92f3f..3c71f1edd892647446c75b0a35e3f72ed574d568 100644
    --- a/core/src/main/java/hudson/model/ModelObject.java
    +++ b/core/src/main/java/hudson/model/ModelObject.java
    @@ -23,15 +23,18 @@
      */
     package hudson.model;
     
    +import jenkins.security.stapler.StaplerAccessibleType;
    +
     /**
      * A model object has a human readable name.
      *
      * And it normally has URL, but this interface doesn't define one.
    - * (Since there're so many classes that define the <tt>getUrl</tt> method
    + * (Since there're so many classes that define the {@code getUrl} method
      * we should have such one.)
      *
      * @author Kohsuke Kawaguchi
      */
    +@StaplerAccessibleType
     public interface ModelObject {
         String getDisplayName();
     }
    diff --git a/core/src/main/java/hudson/model/MultiStageTimeSeries.java b/core/src/main/java/hudson/model/MultiStageTimeSeries.java
    index 4f1071b7d4227f069173c79cac04701c30c3b6ec..62867f2e26a0f24bc53709d6860bcf14bb3f23dd 100644
    --- a/core/src/main/java/hudson/model/MultiStageTimeSeries.java
    +++ b/core/src/main/java/hudson/model/MultiStageTimeSeries.java
    @@ -23,7 +23,7 @@
      */
     package hudson.model;
     
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import hudson.util.NoOverlapCategoryAxis;
     import hudson.util.ChartUtil;
     
    @@ -152,9 +152,9 @@ public class MultiStageTimeSeries implements Serializable {
          * Choose which datapoint to use.
          */
         public enum TimeScale {
    -        SEC10(TimeUnit2.SECONDS.toMillis(10)),
    -        MIN(TimeUnit2.MINUTES.toMillis(1)),
    -        HOUR(TimeUnit2.HOURS.toMillis(1));
    +        SEC10(TimeUnit.SECONDS.toMillis(10)),
    +        MIN(TimeUnit.MINUTES.toMillis(1)),
    +        HOUR(TimeUnit.HOURS.toMillis(1));
     
             /**
              * Number of milliseconds (10 secs, 1 min, and 1 hour)
    diff --git a/core/src/main/java/hudson/model/MyViewsProperty.java b/core/src/main/java/hudson/model/MyViewsProperty.java
    index 8e823efd5a08315ed02d83a5d060bc9d56e033d5..c7852d926d82b5379a5482dc27fcba04751996e7 100644
    --- a/core/src/main/java/hudson/model/MyViewsProperty.java
    +++ b/core/src/main/java/hudson/model/MyViewsProperty.java
    @@ -39,6 +39,7 @@ import java.util.Collections;
     import java.util.List;
     import java.util.concurrent.CopyOnWriteArrayList;
     
    +import javax.annotation.CheckForNull;
     import javax.servlet.ServletException;
     
     import jenkins.model.Jenkins;
    @@ -46,6 +47,8 @@ import net.sf.json.JSONObject;
     
     import org.acegisecurity.AccessDeniedException;
     import org.jenkinsci.Symbol;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.DataBoundConstructor;
     import org.kohsuke.stapler.HttpRedirect;
     import org.kohsuke.stapler.HttpResponse;
    @@ -61,6 +64,12 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
      * @author Tom Huybrechts
      */
     public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback {
    +
    +    /**
    +     * Name of the primary view defined by the user.
    +     * {@code null} means that the View is not defined.
    +     */
    +    @CheckForNull
         private String primaryViewName;
     
         /**
    @@ -71,14 +80,16 @@ public class MyViewsProperty extends UserProperty implements ModifiableViewGroup
         private transient ViewGroupMixIn viewGroupMixIn;
     
         @DataBoundConstructor
    -    public MyViewsProperty(String primaryViewName) {
    +    public MyViewsProperty(@CheckForNull String primaryViewName) {
             this.primaryViewName = primaryViewName;
    +        readResolve(); // initialize fields
         }
     
         private MyViewsProperty() {
    -        readResolve();
    +        this(null);
         }
     
    +    @Restricted(NoExternalUse.class)
         public Object readResolve() {
             if (views == null)
                 // this shouldn't happen, but an error in 1.319 meant the last view could be deleted
    @@ -88,7 +99,10 @@ public class MyViewsProperty extends UserProperty implements ModifiableViewGroup
                 // preserve the non-empty invariant
                 views.add(new AllView(AllView.DEFAULT_VIEW_NAME, this));
             }
    -        primaryViewName = AllView.migrateLegacyPrimaryAllViewLocalizedName(views, primaryViewName);
    +        if (primaryViewName != null) {
    +            // It may happen when the default constructor is invoked
    +            primaryViewName = AllView.migrateLegacyPrimaryAllViewLocalizedName(views, primaryViewName);
    +        }
     
             viewGroupMixIn = new ViewGroupMixIn(this) {
                 protected List<View> views() { return views; }
    @@ -99,11 +113,17 @@ public class MyViewsProperty extends UserProperty implements ModifiableViewGroup
             return this;
         }
     
    +    @CheckForNull
         public String getPrimaryViewName() {
             return primaryViewName;
         }
     
    -    public void setPrimaryViewName(String primaryViewName) {
    +    /**
    +     * Sets the primary view.
    +     * @param primaryViewName Name of the primary view to be set.
    +     *                        {@code null} to make the primary view undefined.
    +     */
    +    public void setPrimaryViewName(@CheckForNull String primaryViewName) {
             this.primaryViewName = primaryViewName;
         }
     
    @@ -185,14 +205,6 @@ public class MyViewsProperty extends UserProperty implements ModifiableViewGroup
             return user.getACL();
         }
     
    -    public void checkPermission(Permission permission) throws AccessDeniedException {
    -        getACL().checkPermission(permission);
    -    }
    -
    -    public boolean hasPermission(Permission permission) {
    -        return getACL().hasPermission(permission);
    -    }
    -
         ///// Action methods /////
         public String getDisplayName() {
             return Messages.MyViewsProperty_DisplayName();
    diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java
    index 42b40a17d207ee7cbe04b6fdaf58903498440477..c61ee7abc138a0dc824f2ae1c76abf916d30e3a7 100644
    --- a/core/src/main/java/hudson/model/Node.java
    +++ b/core/src/main/java/hudson/model/Node.java
    @@ -40,6 +40,7 @@ import hudson.remoting.VirtualChannel;
     import hudson.security.ACL;
     import hudson.security.AccessControlled;
     import hudson.security.Permission;
    +import hudson.slaves.Cloud;
     import hudson.slaves.ComputerListener;
     import hudson.slaves.NodeDescriptor;
     import hudson.slaves.NodeProperty;
    @@ -61,10 +62,13 @@ import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import jenkins.model.Jenkins;
    +import jenkins.util.SystemProperties;
     import jenkins.util.io.OnMaster;
     import net.sf.json.JSONObject;
     import org.acegisecurity.Authentication;
     import org.jvnet.localizer.Localizable;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.ProtectedExternally;
     import org.kohsuke.stapler.BindInterceptor;
     import org.kohsuke.stapler.Stapler;
     import org.kohsuke.stapler.StaplerRequest;
    @@ -94,6 +98,9 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
     
         private static final Logger LOGGER = Logger.getLogger(Node.class.getName());
     
    +    /** @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-46652">JENKINS-46652</a> */
    +    public static /* not final */ boolean SKIP_BUILD_CHECK_ON_FLYWEIGHTS = SystemProperties.getBoolean(Node.class.getName() + ".SKIP_BUILD_CHECK_ON_FLYWEIGHTS", true);
    +
         /**
          * Newly copied agents get this flag set, so that Jenkins doesn't try to start/remove this node until its configuration
          * is saved once.
    @@ -214,8 +221,13 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
     
         /**
          * Creates a new {@link Computer} object that acts as the UI peer of this {@link Node}.
    +     * 
          * Nobody but {@link Jenkins#updateComputerList()} should call this method.
    +     * @return Created instance of the computer.
    +     *         Can be {@code null} if the {@link Node} implementation does not support it (e.g. {@link Cloud} agent).
          */
    +    @CheckForNull
    +    @Restricted(ProtectedExternally.class)
         protected abstract Computer createComputer();
     
         /**
    @@ -337,6 +349,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
         /**
          * Gets the special label that represents this node itself.
          */
    +    @Nonnull
         @WithBridgeMethods(Label.class)
         public LabelAtom getSelfLabel() {
             return LabelAtom.get(getNodeName());
    @@ -386,7 +399,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
             }
     
             Authentication identity = item.authenticate();
    -        if (!getACL().hasPermission(identity,Computer.BUILD)) {
    +        if (!(SKIP_BUILD_CHECK_ON_FLYWEIGHTS && item.task instanceof Queue.FlyweightTask) && !hasPermission(identity, Computer.BUILD)) {
                 // doesn't have a permission
                 return CauseOfBlockage.fromMessage(Messages._Node_LackingBuildPermission(identity.getName(), getDisplayName()));
             }
    @@ -501,14 +514,6 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
             return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
         }
     
    -    public final void checkPermission(Permission permission) {
    -        getACL().checkPermission(permission);
    -    }
    -
    -    public final boolean hasPermission(Permission permission) {
    -        return getACL().hasPermission(permission);
    -    }
    -
         public Node reconfigure(final StaplerRequest req, JSONObject form) throws FormException {
             if (form==null)     return null;
     
    diff --git a/core/src/main/java/hudson/model/ParameterDefinition.java b/core/src/main/java/hudson/model/ParameterDefinition.java
    index fa08b2f0f24ddd71bdb1ac1bcc3d5c8481f0dbe6..d535a90a051433cb5050b96730e05404a1a0f1e4 100644
    --- a/core/src/main/java/hudson/model/ParameterDefinition.java
    +++ b/core/src/main/java/hudson/model/ParameterDefinition.java
    @@ -75,18 +75,18 @@ import org.kohsuke.stapler.export.ExportedBean;
      *
      * <h2>Persistence</h2>
      * <p>
    - * Instances of {@link ParameterDefinition}s are persisted into job <tt>config.xml</tt>
    + * Instances of {@link ParameterDefinition}s are persisted into job {@code config.xml}
      * through XStream.
      *
      *
      * <h2>Associated Views</h2>
      * <h3>config.jelly</h3>
    - * {@link ParameterDefinition} class uses <tt>config.jelly</tt> to contribute a form
    + * {@link ParameterDefinition} class uses {@code config.jelly} to contribute a form
      * fragment in the job configuration screen. Values entered there are fed back to
      * {@link ParameterDescriptor#newInstance(StaplerRequest, JSONObject)} to create {@link ParameterDefinition}s.
      *
      * <h3>index.jelly</h3>
    - * The <tt>index.jelly</tt> view contributes a form fragment in the page where the user
    + * The {@code index.jelly} view contributes a form fragment in the page where the user
      * enters actual values of parameters for a build. The result of this form submission
      * is then fed to {@link ParameterDefinition#createValue(StaplerRequest, JSONObject)} to
      * create {@link ParameterValue}s.
    diff --git a/core/src/main/java/hudson/model/ParameterValue.java b/core/src/main/java/hudson/model/ParameterValue.java
    index 6cd1f46a190d322cb7cf8fbe0cbf6ccba4e3a66a..e64413d94bbaea518149a7d7b6a21e5f9d9e9781 100644
    --- a/core/src/main/java/hudson/model/ParameterValue.java
    +++ b/core/src/main/java/hudson/model/ParameterValue.java
    @@ -39,6 +39,7 @@ import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
     import jenkins.model.Jenkins;
     
    +import jenkins.security.stapler.StaplerAccessibleType;
     import net.sf.json.JSONObject;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.DoNotUse;
    @@ -56,12 +57,12 @@ import org.kohsuke.stapler.export.ExportedBean;
      *
      * <h2>Persistence</h2>
      * <p>
    - * Instances of {@link ParameterValue}s are persisted into build's <tt>build.xml</tt>
    + * Instances of {@link ParameterValue}s are persisted into build's {@code build.xml}
      * through XStream (via {@link ParametersAction}), so instances need to be persistable.
      *
      * <h2>Associated Views</h2>
      * <h3>value.jelly</h3>
    - * The <tt>value.jelly</tt> view contributes a UI fragment to display the parameter
    + * The {@code value.jelly} view contributes a UI fragment to display the parameter
      * values used for a build.
      *
      * <h2>Notes</h2>
    @@ -75,6 +76,7 @@ import org.kohsuke.stapler.export.ExportedBean;
      * @see ParametersAction
      */
     @ExportedBean(defaultVisibility=3)
    +@StaplerAccessibleType
     public abstract class ParameterValue implements Serializable {
     
         private static final Logger LOGGER = Logger.getLogger(ParameterValue.class.getName());
    diff --git a/core/src/main/java/hudson/model/ParametersAction.java b/core/src/main/java/hudson/model/ParametersAction.java
    index fa2ed5b013861f3595e4cb24f6c42a240c63f6dc..9de8ef8dcd00e9f390cf59ee2b5d269ce7019e13 100644
    --- a/core/src/main/java/hudson/model/ParametersAction.java
    +++ b/core/src/main/java/hudson/model/ParametersAction.java
    @@ -87,7 +87,7 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
     
         private Set<String> safeParameters;
     
    -    private final List<ParameterValue> parameters;
    +    private @Nonnull List<ParameterValue> parameters;
     
         private List<String> parameterDefinitionNames;
     
    @@ -99,8 +99,8 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
     
         private transient Run<?, ?> run;
     
    -    public ParametersAction(List<ParameterValue> parameters) {
    -        this.parameters = parameters;
    +    public ParametersAction(@Nonnull List<ParameterValue> parameters) {
    +        this.parameters = new ArrayList<>(parameters);
             String paramNames = SystemProperties.getString(SAFE_PARAMETERS_SYSTEM_PROPERTY_NAME);
             safeParameters = new TreeSet<>();
             if (paramNames != null) {
    @@ -138,10 +138,11 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
             }
         }
     
    -    public void buildEnvVars(AbstractBuild<?,?> build, EnvVars env) {
    +    @Override
    +    public void buildEnvironment(Run<?,?> run, EnvVars env) {
             for (ParameterValue p : getParameters()) {
                 if (p == null) continue;
    -            p.buildEnvironment(build, env); 
    +            p.buildEnvironment(run, env);
             }
         }
     
    @@ -283,6 +284,9 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
         }
     
         private Object readResolve() {
    +        if (parameters == null) { // JENKINS-39495
    +            parameters = Collections.emptyList();
    +        }
             if (build != null)
                 OldDataMonitor.report(build, "1.283");
             if (safeParameters == null) {
    @@ -295,7 +299,7 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
         public void onAttached(Run<?, ?> r) {
             ParametersDefinitionProperty p = r.getParent().getProperty(ParametersDefinitionProperty.class);
             if (p != null) {
    -            this.parameterDefinitionNames = p.getParameterDefinitionNames();
    +            this.parameterDefinitionNames = new ArrayList<>(p.getParameterDefinitionNames());
             } else {
                 this.parameterDefinitionNames = Collections.emptyList();
             }
    @@ -316,8 +320,8 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
                 return parameters;
             }
     
    -        String shouldKeepFlag = SystemProperties.getString(KEEP_UNDEFINED_PARAMETERS_SYSTEM_PROPERTY_NAME);
    -        if ("true".equalsIgnoreCase(shouldKeepFlag)) {
    +        Boolean shouldKeepFlag = SystemProperties.optBoolean(KEEP_UNDEFINED_PARAMETERS_SYSTEM_PROPERTY_NAME);
    +        if (shouldKeepFlag != null && shouldKeepFlag.booleanValue()) {
                 return parameters;
             }
     
    @@ -326,7 +330,7 @@ public class ParametersAction implements RunAction2, Iterable<ParameterValue>, Q
             for (ParameterValue v : this.parameters) {
                 if (this.parameterDefinitionNames.contains(v.getName()) || isSafeParameter(v.getName())) {
                     filteredParameters.add(v);
    -            } else if ("false".equalsIgnoreCase(shouldKeepFlag)) {
    +            } else if (shouldKeepFlag == null) {
                     LOGGER.log(Level.WARNING, "Skipped parameter `{0}` as it is undefined on `{1}`. Set `-D{2}=true` to allow "
                             + "undefined parameters to be injected as environment variables or `-D{3}=[comma-separated list]` to whitelist specific parameter names, "
                             + "even though it represents a security breach or `-D{2}=false` to no longer show this message.",
    diff --git a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java
    index 956812158d7b695354f1aa108fe81b297e92e1a2..1fa26ed3720af73f034e3325cc80bd5db293e76d 100644
    --- a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java
    +++ b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java
    @@ -34,6 +34,7 @@ import java.util.Arrays;
     import java.util.Collection;
     import java.util.Collections;
     import java.util.List;
    +import java.util.concurrent.TimeUnit;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import javax.servlet.ServletException;
    @@ -58,7 +59,7 @@ import org.kohsuke.stapler.export.ExportedBean;
      * Keeps a list of the parameters defined for a project.
      *
      * <p>
    - * This class also implements {@link Action} so that <tt>index.jelly</tt> provides
    + * This class also implements {@link Action} so that {@code index.jelly} provides
      * a form to enter build parameters.
      * <p>The owning job needs a {@code sidepanel.jelly} and should have web methods delegating to {@link ParameterizedJobMixIn#doBuild} and {@link ParameterizedJobMixIn#doBuildWithParameters}.
      * The builds also need a {@code sidepanel.jelly}.
    @@ -71,17 +72,11 @@ public class ParametersDefinitionProperty extends OptionalJobProperty<Job<?, ?>>
     
         @DataBoundConstructor
         public ParametersDefinitionProperty(@Nonnull List<ParameterDefinition> parameterDefinitions) {
    -        if (parameterDefinitions == null) {
    -            throw new NullPointerException("ParameterDefinitions is null when this is a not valid value");
    -        }
    -        this.parameterDefinitions = parameterDefinitions;
    +        this.parameterDefinitions = parameterDefinitions != null ? parameterDefinitions : new ArrayList<>();
         }
     
         public ParametersDefinitionProperty(@Nonnull ParameterDefinition... parameterDefinitions) {
    -        if (parameterDefinitions == null) {
    -            throw new NullPointerException("ParameterDefinitions is null when this is a not valid value");
    -        }
    -        this.parameterDefinitions = Arrays.asList(parameterDefinitions) ;
    +        this.parameterDefinitions = parameterDefinitions != null ? Arrays.asList(parameterDefinitions) : new ArrayList<>();
         }
     
         private Object readResolve() {
    @@ -107,15 +102,7 @@ public class ParametersDefinitionProperty extends OptionalJobProperty<Job<?, ?>>
          * Gets the names of all the parameter definitions.
          */
         public List<String> getParameterDefinitionNames() {
    -        return new AbstractList<String>() {
    -            public String get(int index) {
    -                return parameterDefinitions.get(index).getName();
    -            }
    -
    -            public int size() {
    -                return parameterDefinitions.size();
    -            }
    -        };
    +        return new DefinitionsAbstractList(this.parameterDefinitions);
         }
     
         @Nonnull
    @@ -147,7 +134,8 @@ public class ParametersDefinitionProperty extends OptionalJobProperty<Job<?, ?>>
          * This method is supposed to be invoked from {@link ParameterizedJobMixIn#doBuild(StaplerRequest, StaplerResponse, TimeDuration)}.
          */
         public void _doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
    -        if (delay==null)    delay=new TimeDuration(getJob().getQuietPeriod());
    +        if (delay==null)
    +            delay=new TimeDuration(TimeUnit.MILLISECONDS.convert(getJob().getQuietPeriod(), TimeUnit.SECONDS));
     
     
             List<ParameterValue> values = new ArrayList<ParameterValue>();
    @@ -171,7 +159,7 @@ public class ParametersDefinitionProperty extends OptionalJobProperty<Job<?, ?>>
             }
     
         	WaitingItem item = Jenkins.getInstance().getQueue().schedule(
    -                getJob(), delay.getTime(), new ParametersAction(values), new CauseAction(new Cause.UserIdCause()));
    +                getJob(), delay.getTimeInSeconds(), new ParametersAction(values), new CauseAction(new Cause.UserIdCause()));
             if (item!=null) {
                 String url = formData.optString("redirectTo");
                 if (url==null || !Util.isSafeToRedirectTo(url))   // avoid open redirect
    @@ -196,10 +184,11 @@ public class ParametersDefinitionProperty extends OptionalJobProperty<Job<?, ?>>
             		values.add(value);
             	}
             }
    -        if (delay==null)    delay=new TimeDuration(getJob().getQuietPeriod());
    +        if (delay==null)
    +            delay=new TimeDuration(TimeUnit.MILLISECONDS.convert(getJob().getQuietPeriod(), TimeUnit.SECONDS));
     
             Queue.Item item = Jenkins.getInstance().getQueue().schedule2(
    -                getJob(), delay.getTime(), new ParametersAction(values), ParameterizedJobMixIn.getBuildCause(getJob(), req)).getItem();
    +                getJob(), delay.getTimeInSeconds(), new ParametersAction(values), ParameterizedJobMixIn.getBuildCause(getJob(), req)).getItem();
     
             if (item != null) {
                 rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl());
    @@ -252,4 +241,20 @@ public class ParametersDefinitionProperty extends OptionalJobProperty<Job<?, ?>>
         public String getUrlName() {
             return null;
         }
    +
    +    private static class DefinitionsAbstractList extends AbstractList<String> {
    +        private final List<ParameterDefinition> parameterDefinitions;
    +
    +        public DefinitionsAbstractList(List<ParameterDefinition> parameterDefinitions) {
    +            this.parameterDefinitions = parameterDefinitions;
    +        }
    +
    +        public String get(int index) {
    +            return this.parameterDefinitions.get(index).getName();
    +        }
    +
    +        public int size() {
    +            return this.parameterDefinitions.size();
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/PeriodicWork.java b/core/src/main/java/hudson/model/PeriodicWork.java
    index aab83426723546989179c0401803d845cfceccdc..1079553551bf02620ca92ba89ce80444d556a7d7 100644
    --- a/core/src/main/java/hudson/model/PeriodicWork.java
    +++ b/core/src/main/java/hudson/model/PeriodicWork.java
    @@ -23,6 +23,7 @@
      */
     package hudson.model;
     
    +import hudson.ExtensionListListener;
     import hudson.init.Initializer;
     import hudson.triggers.SafeTimerTask;
     import hudson.ExtensionPoint;
    @@ -30,6 +31,8 @@ import hudson.Extension;
     import hudson.ExtensionList;
     import jenkins.util.Timer;
     
    +import java.util.HashSet;
    +import java.util.Set;
     import java.util.concurrent.TimeUnit;
     import java.util.logging.Logger;
     import java.util.Random;
    @@ -99,15 +102,49 @@ public abstract class PeriodicWork extends SafeTimerTask implements ExtensionPoi
         @Initializer(after= JOB_LOADED)
         public static void init() {
             // start all PeriodicWorks
    -        for (PeriodicWork p : PeriodicWork.all()) {
    -            Timer.get().scheduleAtFixedRate(p, p.getInitialDelay(), p.getRecurrencePeriod(), TimeUnit.MILLISECONDS);
    +        ExtensionList<PeriodicWork> extensionList = all();
    +        extensionList.addListener(new PeriodicWorkExtensionListListener(extensionList));
    +        for (PeriodicWork p : extensionList) {
    +            schedulePeriodicWork(p);
             }
         }
     
    +    private static void schedulePeriodicWork(PeriodicWork p) {
    +        Timer.get().scheduleAtFixedRate(p, p.getInitialDelay(), p.getRecurrencePeriod(), TimeUnit.MILLISECONDS);
    +    }
    +
         // time constants
         protected static final long MIN = 1000*60;
         protected static final long HOUR =60*MIN;
         protected static final long DAY = 24*HOUR;
     
         private static final Random RANDOM = new Random();
    +
    +    /**
    +     * ExtensionListener that will kick off any new AperiodWork extensions from plugins that are dynamically
    +     * loaded.
    +     */
    +    private static class PeriodicWorkExtensionListListener extends ExtensionListListener {
    +
    +        private final Set<PeriodicWork> registered = new HashSet<>();
    +
    +        PeriodicWorkExtensionListListener(ExtensionList<PeriodicWork> initiallyRegistered) {
    +            for (PeriodicWork p : initiallyRegistered) {
    +                registered.add(p);
    +            }
    +        }
    +
    +        @Override
    +        public void onChange() {
    +            synchronized (registered) {
    +                for (PeriodicWork p : PeriodicWork.all()) {
    +                    // it is possibly to programatically remove Extensions but that is rarely used.
    +                    if (!registered.contains(p)) {
    +                        schedulePeriodicWork(p);
    +                        registered.add(p);
    +                    }
    +                }
    +            }
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/PersistentDescriptor.java b/core/src/main/java/hudson/model/PersistentDescriptor.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..837d154cfb51be87f96d5a40fefa846714ee8d96
    --- /dev/null
    +++ b/core/src/main/java/hudson/model/PersistentDescriptor.java
    @@ -0,0 +1,16 @@
    +package hudson.model;
    +
    +import javax.annotation.PostConstruct;
    +
    +/**
    + * Marker interface for Descriptors which use xml persistent data, and as such need to load from disk when instantiated.
    + * <p>
    + * {@link Descriptor#load()} method is annotated as {@link PostConstruct} so it get automatically invoked after
    + * constructor and field injection.
    + * @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
    + */
    +public interface PersistentDescriptor extends Saveable {
    +
    +    @PostConstruct
    +    void load();
    +}
    diff --git a/core/src/main/java/hudson/model/ProxyView.java b/core/src/main/java/hudson/model/ProxyView.java
    index 795b94ba8635d64757a30386b35afaca22308602..a879dfd0b0d9211ce40018b05ade964ea5d65b10 100644
    --- a/core/src/main/java/hudson/model/ProxyView.java
    +++ b/core/src/main/java/hudson/model/ProxyView.java
    @@ -91,6 +91,11 @@ public class ProxyView extends View implements StaplerFallback {
             return getProxiedView().contains(item);
         }
     
    +    @Override
    +    public TopLevelItem getItem(String name) {
    +        return getProxiedView().getItem(name);
    +    }
    +
         @Override
         protected void submit(StaplerRequest req) throws IOException, ServletException, FormException {
             String proxiedViewName = req.getSubmittedForm().getString("proxiedViewName");
    diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java
    index a5ec695c71a6cf2a3256549bb6d773a3869e39d6..b2c4f7135a9df7b6ce4ffc232011390a4a2f7c4f 100644
    --- a/core/src/main/java/hudson/model/Queue.java
    +++ b/core/src/main/java/hudson/model/Queue.java
    @@ -24,10 +24,12 @@
      */
     package hudson.model;
     
    +import com.google.common.annotations.VisibleForTesting;
     import com.google.common.cache.Cache;
     import com.google.common.cache.CacheBuilder;
     import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
     import hudson.BulkChange;
    +import hudson.Extension;
     import hudson.ExtensionList;
     import hudson.ExtensionPoint;
     import hudson.Util;
    @@ -64,11 +66,14 @@ import hudson.model.queue.WorkUnitContext;
     import hudson.security.ACL;
     import hudson.security.AccessControlled;
     import java.nio.file.Files;
    -import java.nio.file.InvalidPathException;
    +
    +import hudson.util.Futures;
     import jenkins.security.QueueItemAuthenticatorProvider;
    +import jenkins.security.stapler.StaplerAccessibleType;
    +import jenkins.util.SystemProperties;
     import jenkins.util.Timer;
     import hudson.triggers.SafeTimerTask;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import hudson.util.XStream2;
     import hudson.util.ConsistentHash;
     import hudson.util.ConsistentHash.Hash;
    @@ -76,8 +81,8 @@ import hudson.util.ConsistentHash.Hash;
     import java.io.BufferedReader;
     import java.io.File;
     import java.io.IOException;
    -import java.io.InputStreamReader;
     import java.lang.ref.WeakReference;
    +import java.nio.charset.Charset;
     import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Calendar;
    @@ -94,7 +99,6 @@ import java.util.NoSuchElementException;
     import java.util.Set;
     import java.util.TreeSet;
     import java.util.concurrent.Callable;
    -import java.util.concurrent.TimeUnit;
     import java.util.concurrent.Future;
     import java.util.concurrent.atomic.AtomicLong;
     import java.util.concurrent.locks.Condition;
    @@ -103,6 +107,7 @@ import java.util.logging.Level;
     import java.util.logging.Logger;
     
     import javax.annotation.Nonnull;
    +import javax.annotation.concurrent.GuardedBy;
     import javax.servlet.ServletException;
     
     import jenkins.model.Jenkins;
    @@ -377,15 +382,13 @@ public class Queue extends ResourceController implements Saveable {
                 // first try the old format
                 File queueFile = getQueueFile();
                 if (queueFile.exists()) {
    -                try (BufferedReader in = new BufferedReader(new InputStreamReader(Files.newInputStream(queueFile.toPath())))) {
    +                try (BufferedReader in = Files.newBufferedReader(Util.fileToPath(queueFile), Charset.defaultCharset())) {
                         String line;
                         while ((line = in.readLine()) != null) {
                             AbstractProject j = Jenkins.getInstance().getItemByFullName(line, AbstractProject.class);
                             if (j != null)
                                 j.scheduleBuild();
                         }
    -                } catch (InvalidPathException e) {
    -                    throw new IOException(e);
                     }
                     // discard the queue file now that we are done
                     queueFile.delete();
    @@ -457,6 +460,9 @@ public class Queue extends ResourceController implements Saveable {
          */
         public void save() {
             if(BulkChange.contains(this))  return;
    +        if (Jenkins.getInstanceOrNull() == null) {
    +            return;
    +        }
     
             XmlFile queueFile = new XmlFile(XSTREAM, getXMLQueueFile());
             lock.lock();
    @@ -502,11 +508,11 @@ public class Queue extends ResourceController implements Saveable {
         }
     
         private File getQueueFile() {
    -        return new File(Jenkins.getInstance().getRootDir(), "queue.txt");
    +        return new File(Jenkins.get().getRootDir(), "queue.txt");
         }
     
         /*package*/ File getXMLQueueFile() {
    -        return new File(Jenkins.getInstance().getRootDir(), "queue.xml");
    +        return new File(Jenkins.get().getRootDir(), "queue.xml");
         }
     
         /**
    @@ -755,7 +761,9 @@ public class Queue extends ResourceController implements Saveable {
         public HttpResponse doCancelItem(@QueryParameter long id) throws IOException, ServletException {
             Item item = getItem(id);
             if (item != null) {
    -            cancel(item);
    +            if(item.hasCancelPermission()){
    +                cancel(item);
    +            }
             } // else too late, ignore (JENKINS-14813)
             return HttpResponses.forwardToPreviousPage();
         }
    @@ -1105,7 +1113,7 @@ public class Queue extends ResourceController implements Saveable {
         /**
          * Gets the information about the queue item for the given project.
          *
    -     * @return null if the project is not in the queue.
    +     * @return empty if the project is not in the queue.
          */
         public List<Item> getItems(Task t) {
             Snapshot snapshot = this.snapshot;
    @@ -1175,28 +1183,63 @@ public class Queue extends ResourceController implements Saveable {
         /**
          * Checks if the given item should be prevented from entering into the {@link #buildables} state
          * and instead stay in the {@link #blockedProjects} state.
    +     *
    +     * @return the reason of blockage if it exists null otherwise.
          */
    -    private boolean isBuildBlocked(Item i) {
    -        if (i.task.isBuildBlocked() || !canRun(i.task.getResourceList()))
    -            return true;
    +    @CheckForNull
    +    private CauseOfBlockage getCauseOfBlockageForItem(Item i) {
    +        CauseOfBlockage causeOfBlockage = getCauseOfBlockageForTask(i.task);
    +        if (causeOfBlockage != null) {
    +            return causeOfBlockage;
    +        }
     
             for (QueueTaskDispatcher d : QueueTaskDispatcher.all()) {
    -            if (d.canRun(i)!=null)
    -                return true;
    +            causeOfBlockage = d.canRun(i);
    +            if (causeOfBlockage != null)
    +                return causeOfBlockage;
    +        }
    +
    +        if(!(i instanceof BuildableItem)) {
    +            // Make sure we don't queue two tasks of the same project to be built
    +            // unless that project allows concurrent builds. Once item is buildable it's ok.
    +            //
    +            // This check should never pass. And must be remove once we can completely rely on `getCauseOfBlockage`.
    +            // If `task.isConcurrentBuild` returns `false`,
    +            // it should also return non-null value for `task.getCauseOfBlockage` in case of on-going execution.
    +            // But both are public non-final methods, so, we need to keep backward compatibility here.
    +            // And check one more time across all `buildables` and `pendings` for O(N) each.
    +            if (!i.task.isConcurrentBuild() && (buildables.containsKey(i.task) || pendings.containsKey(i.task))) {
    +                return CauseOfBlockage.fromMessage(Messages._Queue_InProgress());
    +            }
             }
     
    -        return false;
    +        return null;
         }
     
         /**
    -     * Make sure we don't queue two tasks of the same project to be built
    -     * unless that project allows concurrent builds.
    +     *
    +     * Checks if the given task knows the reasons to be blocked or it needs some unavailable resources
    +     *
    +     * @param task the task.
    +     * @return the reason of blockage if it exists null otherwise.
          */
    -    private boolean allowNewBuildableTask(Task t) {
    -        if (t.isConcurrentBuild()) {
    -            return true;
    +    @CheckForNull
    +    private CauseOfBlockage getCauseOfBlockageForTask(Task task) {
    +        CauseOfBlockage causeOfBlockage = task.getCauseOfBlockage();
    +        if (causeOfBlockage != null) {
    +            return task.getCauseOfBlockage();
             }
    -        return !buildables.containsKey(t) && !pendings.containsKey(t);
    +
    +        if (!canRun(task.getResourceList())) {
    +            ResourceActivity r = getBlockingActivity(task);
    +            if (r != null) {
    +                if (r == task) // blocked by itself, meaning another build is in progress
    +                    return CauseOfBlockage.fromMessage(Messages._Queue_InProgress());
    +                return CauseOfBlockage.fromMessage(Messages._Queue_BlockedBy(r.getDisplayName()));
    +            }
    +        }
    +
    +        return null;
         }
     
         /**
    @@ -1413,6 +1456,10 @@ public class Queue extends ResourceController implements Saveable {
          * and it also gets invoked periodically (see {@link Queue.MaintainTask}.)
          */
         public void maintain() {
    +        Jenkins jenkins = Jenkins.getInstanceOrNull();
    +        if (jenkins == null) {
    +            return;
    +        }
             lock.lock();
             try { try {
     
    @@ -1423,8 +1470,8 @@ public class Queue extends ResourceController implements Saveable {
     
                 {// update parked (and identify any pending items whose executor has disappeared)
                     List<BuildableItem> lostPendings = new ArrayList<BuildableItem>(pendings);
    -                for (Computer c : Jenkins.getInstance().getComputers()) {
    -                    for (Executor e : c.getExecutors()) {
    +                for (Computer c : jenkins.getComputers()) {
    +                    for (Executor e : c.getAllExecutors()) {
                             if (e.isInterrupted()) {
                                 // JENKINS-28840 we will deadlock if we try to touch this executor while interrupt flag set
                                 // we need to clear lost pendings as we cannot know what work unit was on this executor
    @@ -1472,7 +1519,8 @@ public class Queue extends ResourceController implements Saveable {
                     for (BlockedItem p : blockedItems) {
                         String taskDisplayName = LOGGER.isLoggable(Level.FINEST) ? p.task.getFullDisplayName() : null;
                         LOGGER.log(Level.FINEST, "Current blocked item: {0}", taskDisplayName);
    -                    if (!isBuildBlocked(p) && allowNewBuildableTask(p.task)) {
    +                    CauseOfBlockage causeOfBlockage = getCauseOfBlockageForItem(p);
    +                    if (causeOfBlockage == null) {
                             LOGGER.log(Level.FINEST,
                                     "BlockedItem {0}: blocked -> buildable as the build is not blocked and new tasks are allowed",
                                     taskDisplayName);
    @@ -1487,6 +1535,8 @@ public class Queue extends ResourceController implements Saveable {
                                 // determine if they are blocked by the lucky winner
                                 updateSnapshot();
                             }
    +                    } else {
    +                        p.setCauseOfBlockage(causeOfBlockage);
                         }
                     }
                 }
    @@ -1501,8 +1551,8 @@ public class Queue extends ResourceController implements Saveable {
                     }
     
                     top.leave(this);
    -                Task p = top.task;
    -                if (!isBuildBlocked(top) && allowNewBuildableTask(p)) {
    +                CauseOfBlockage causeOfBlockage = getCauseOfBlockageForItem(top);
    +                if (causeOfBlockage == null) {
                         // ready to be executed immediately
                         Runnable r = makeBuildable(new BuildableItem(top));
                         String topTaskDisplayName = LOGGER.isLoggable(Level.FINEST) ? top.task.getFullDisplayName() : null;
    @@ -1511,17 +1561,24 @@ public class Queue extends ResourceController implements Saveable {
                             r.run();
                         } else {
                             LOGGER.log(Level.FINEST, "Item {0} was unable to be made a buildable and is now a blocked item.", topTaskDisplayName);
    -                        new BlockedItem(top).enter(this);
    +                        new BlockedItem(top, CauseOfBlockage.fromMessage(Messages._Queue_HudsonIsAboutToShutDown())).enter(this);
                         }
                     } else {
                         // this can't be built now because another build is in progress
                         // set this project aside.
    -                    new BlockedItem(top).enter(this);
    +                    new BlockedItem(top, causeOfBlockage).enter(this);
                     }
                 }
     
    -            if (s != null)
    -                s.sortBuildableItems(buildables);
    +            if (s != null) {
    +                try {
    +                    s.sortBuildableItems(buildables);
    +                } catch (Throwable e) {
    +                    // We don't really care if the sort doesn't sort anything, we still should
    +                    // continue to do our job. We'll complain about it and continue.
    +                    LOGGER.log(Level.WARNING, "s.sortBuildableItems() threw Throwable: {0}", e);
    +                }
    +            }
                 
                 // Ensure that identification of blocked tasks is using the live state: JENKINS-27708 & JENKINS-27871
                 updateSnapshot();
    @@ -1530,9 +1587,10 @@ public class Queue extends ResourceController implements Saveable {
                 for (BuildableItem p : new ArrayList<BuildableItem>(
                         buildables)) {// copy as we'll mutate the list in the loop
                     // one last check to make sure this build is not blocked.
    -                if (isBuildBlocked(p)) {
    +                CauseOfBlockage causeOfBlockage = getCauseOfBlockageForItem(p);
    +                if (causeOfBlockage != null) {
                         p.leave(this);
    -                    new BlockedItem(p).enter(this);
    +                    new BlockedItem(p, causeOfBlockage).enter(this);
                         LOGGER.log(Level.FINE, "Catching that {0} is blocked in the last minute", p);
                         // JENKINS-28926 we have moved an unblocked task into the blocked state, update snapshot
                         // so that other buildables which might have been blocked by this can see the state change
    @@ -1540,7 +1598,7 @@ public class Queue extends ResourceController implements Saveable {
                         continue;
                     }
     
    -                String taskDisplayName = p.task.getFullDisplayName();
    +                String taskDisplayName = LOGGER.isLoggable(Level.FINEST) ? p.task.getFullDisplayName() : null;
     
                     if (p.task instanceof FlyweightTask) {
                         Runnable r = makeFlyWeightTaskBuildable(new BuildableItem(p));
    @@ -1596,7 +1654,7 @@ public class Queue extends ResourceController implements Saveable {
                         // The creation of a snapshot itself should be relatively cheap given the expected rate of
                         // job execution. You probably would need 100's of jobs starting execution every iteration
                         // of maintain() before this could even start to become an issue and likely the calculation
    -                    // of isBuildBlocked(p) will become a bottleneck before updateSnapshot() will. Additionally
    +                    // of getCauseOfBlockageForItem(p) will become a bottleneck before updateSnapshot() will. Additionally
                         // since the snapshot itself only ever has at most one reference originating outside of the stack
                         // it should remain in the eden space and thus be cheap to GC.
                         // See https://jenkins-ci.org/issue/27708?focusedCommentId=225819&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-225819
    @@ -1617,7 +1675,7 @@ public class Queue extends ResourceController implements Saveable {
          */
         private @CheckForNull Runnable makeBuildable(final BuildableItem p) {
             if (p.task instanceof FlyweightTask) {
    -            String taskDisplayName = p.task.getFullDisplayName();
    +            String taskDisplayName = LOGGER.isLoggable(Level.FINEST) ? p.task.getFullDisplayName() : null;
                 if (!isBlockedByShutdown(p.task)) {
     
                     Runnable runnable = makeFlyWeightTaskBuildable(p);
    @@ -1652,6 +1710,17 @@ public class Queue extends ResourceController implements Saveable {
             //we double check if this is a flyweight task
             if (p.task instanceof FlyweightTask) {
                 Jenkins h = Jenkins.getInstance();
    +
    +            Label lbl = p.getAssignedLabel();
    +
    +            if (lbl != null && lbl.equals(h.getSelfLabel())) {
    +                if (h.canTake(p) == null) {
    +                    return createFlyWeightTaskRunnable(p, h.toComputer());
    +                } else {
    +                    return null;
    +                }
    +            }
    +
                 Map<Node, Integer> hashSource = new HashMap<Node, Integer>(h.getNodes().size());
     
                 // Even if master is configured with zero executors, we may need to run a flyweight task like MatrixProject on it.
    @@ -1664,8 +1733,8 @@ public class Queue extends ResourceController implements Saveable {
                 ConsistentHash<Node> hash = new ConsistentHash<Node>(NODE_HASH);
                 hash.addAll(hashSource);
     
    -            Label lbl = p.getAssignedLabel();
    -            for (Node n : hash.list(p.task.getFullDisplayName())) {
    +            String fullDisplayName = p.task.getFullDisplayName();
    +            for (Node n : hash.list(fullDisplayName)) {
                     final Computer c = n.toComputer();
                     if (c == null || c.isOffline()) {
                         continue;
    @@ -1677,18 +1746,25 @@ public class Queue extends ResourceController implements Saveable {
                         continue;
                     }
     
    -                LOGGER.log(Level.FINEST, "Creating flyweight task {0} for computer {1}", new Object[]{p.task.getFullDisplayName(), c.getName()});
    -                return new Runnable() {
    -                    @Override public void run() {
    -                        c.startFlyWeightTask(new WorkUnitContext(p).createWorkUnit(p.task));
    -                        makePending(p);
    -                    }
    -                };
    +                return createFlyWeightTaskRunnable(p, c);
                 }
             }
             return null;
         }
     
    +    private Runnable createFlyWeightTaskRunnable(final BuildableItem p, final Computer c) {
    +        if (LOGGER.isLoggable(Level.FINEST)) {
    +            LOGGER.log(Level.FINEST, "Creating flyweight task {0} for computer {1}",
    +                    new Object[]{p.task.getFullDisplayName(), c.getName()});
    +        }
    +        return new Runnable() {
    +            @Override public void run() {
    +                c.startFlyWeightTask(new WorkUnitContext(p).createWorkUnit(p.task));
    +                makePending(p);
    +            }
    +        };
    +    }
    +
         private static Hash<Node> NODE_HASH = new Hash<Node>() {
             public String hash(Node node) {
                 return node.getNodeName();
    @@ -1774,18 +1850,22 @@ public class Queue extends ResourceController implements Saveable {
             /**
              * Returns true if the execution should be blocked
              * for temporary reasons.
    -         *
    -         * <p>
    -         * Short-hand for {@code getCauseOfBlockage()!=null}.
    +         * @deprecated Use {@link #getCauseOfBlockage} != null
              */
    -        boolean isBuildBlocked();
    +        @Deprecated
    +        default boolean isBuildBlocked() {
    +            return getCauseOfBlockage() != null;
    +        }
     
             /**
              * @deprecated as of 1.330
              *      Use {@link CauseOfBlockage#getShortDescription()} instead.
              */
             @Deprecated
    -        String getWhyBlocked();
    +        default String getWhyBlocked() {
    +            CauseOfBlockage cause = getCauseOfBlockage();
    +            return cause != null ? cause.getShortDescription() : null;
    +        }
     
             /**
              * If the execution of this task should be blocked for temporary reasons,
    @@ -1817,6 +1897,18 @@ public class Queue extends ResourceController implements Saveable {
              */
             String getFullDisplayName();
     
    +        /**
    +         * Returns task-specific key which is used by the {@link LoadBalancer} to choose one particular executor
    +         * amongst all the free executors on all possibly suitable nodes.
    +         * NOTE: To be able to re-use the same node during the next run this key should not change from one run to
    +         * another. You probably want to compute that key based on the job's name.
    +         * <p>
    +         * @return by default: {@link #getFullDisplayName()}
    +         *
    +         * @see hudson.model.LoadBalancer
    +         */
    +        default String getAffinityKey() { return getFullDisplayName(); }
    +
             /**
              * Checks the permission to see if the current user can abort this executable.
              * Returns normally from this method if it's OK.
    @@ -1925,9 +2017,10 @@ public class Queue extends ResourceController implements Saveable {
          *
          * <h2>Views</h2>
          * <p>
    -     * Implementation must have <tt>executorCell.jelly</tt>, which is
    +     * Implementation must have {@code executorCell.jelly}, which is
          * used to render the HTML that indicates this executable is executing.
          */
    +    @StaplerAccessibleType
         public interface Executable extends Runnable {
             /**
              * Task from which this executable was created.
    @@ -2060,6 +2153,7 @@ public class Queue extends ResourceController implements Saveable {
              * <p>
              * This code takes {@link LabelAssignmentAction} into account, then fall back to {@link SubTask#getAssignedLabel()}
              */
    +        @CheckForNull
             public Label getAssignedLabel() {
                 for (LabelAssignmentAction laa : getActions(LabelAssignmentAction.class)) {
                     Label l = laa.getAssignedLabel(task);
    @@ -2125,8 +2219,10 @@ public class Queue extends ResourceController implements Saveable {
                 for (Action action: actions) addAction(action);
             }
     
    +        @SuppressWarnings("deprecation") // JENKINS-51584
             protected Item(Item item) {
    -        	this(item.task, new ArrayList<Action>(item.getAllActions()), item.id, item.future, item.inQueueSince);
    +            // do not use item.getAllActions() here as this will persist actions from a TransientActionFactory
    +            this(item.task, new ArrayList<Action>(item.getActions()), item.id, item.future, item.inQueueSince);
             }
     
             /**
    @@ -2192,7 +2288,9 @@ public class Queue extends ResourceController implements Saveable {
             @Deprecated
             @RequirePOST
             public HttpResponse doCancelQueue() throws IOException, ServletException {
    -        	Jenkins.getInstance().getQueue().cancel(this);
    +            if(hasCancelPermission()){
    +                Jenkins.getInstance().getQueue().cancel(this);
    +            }
                 return HttpResponses.forwardToPreviousPage();
             }
     
    @@ -2216,9 +2314,20 @@ public class Queue extends ResourceController implements Saveable {
                 return task.getDefaultAuthentication(this);
             }
     
    -
    -        public Api getApi() {
    -            return new Api(this);
    +        @Restricted(DoNotUse.class) // only for Stapler export
    +        public Api getApi() throws AccessDeniedException {
    +            if (task instanceof AccessControlled) {
    +                AccessControlled ac = (AccessControlled) task;
    +                if (!ac.hasPermission(hudson.model.Item.DISCOVER)) {
    +                    return null; // same as getItem(long) returning null (details are printed only in case of -Dstapler.trace=true)
    +                } else if (!ac.hasPermission(hudson.model.Item.READ)) {
    +                    throw new AccessDeniedException("Please log in to access " + task.getUrl()); // like Jenkins.getItem
    +                } else { // have READ
    +                    return new Api(this);
    +                }
    +            } else { // err on the safe side
    +                return null;
    +            }
             }
     
             private Object readResolve() {
    @@ -2378,10 +2487,10 @@ public class Queue extends ResourceController implements Saveable {
     
             public CauseOfBlockage getCauseOfBlockage() {
                 long diff = timestamp.getTimeInMillis() - System.currentTimeMillis();
    -            if (diff > 0)
    +            if (diff >= 0)
                     return CauseOfBlockage.fromMessage(Messages._Queue_InQuietPeriod(Util.getTimeSpanString(diff)));
                 else
    -                return CauseOfBlockage.fromMessage(Messages._Queue_Unknown());
    +                return CauseOfBlockage.fromMessage(Messages._Queue_FinishedWaiting());
             }
     
             @Override
    @@ -2442,29 +2551,37 @@ public class Queue extends ResourceController implements Saveable {
          * {@link Item} in the {@link Queue#blockedProjects} stage.
          */
         public final class BlockedItem extends NotWaitingItem {
    +        private transient CauseOfBlockage causeOfBlockage = null;
    +
             public BlockedItem(WaitingItem wi) {
    -            super(wi);
    +            this(wi, null);
             }
     
             public BlockedItem(NotWaitingItem ni) {
    +            this(ni, null);
    +        }
    +
    +        BlockedItem(WaitingItem wi, CauseOfBlockage causeOfBlockage) {
    +            super(wi);
    +            this.causeOfBlockage = causeOfBlockage;
    +        }
    +
    +        BlockedItem(NotWaitingItem ni, CauseOfBlockage causeOfBlockage) {
                 super(ni);
    +            this.causeOfBlockage = causeOfBlockage;
             }
     
    -        public CauseOfBlockage getCauseOfBlockage() {
    -            ResourceActivity r = getBlockingActivity(task);
    -            if (r != null) {
    -                if (r == task) // blocked by itself, meaning another build is in progress
    -                    return CauseOfBlockage.fromMessage(Messages._Queue_InProgress());
    -                return CauseOfBlockage.fromMessage(Messages._Queue_BlockedBy(r.getDisplayName()));
    -            }
    +        void setCauseOfBlockage(CauseOfBlockage causeOfBlockage) {
    +            this.causeOfBlockage = causeOfBlockage;
    +        }
     
    -            for (QueueTaskDispatcher d : QueueTaskDispatcher.all()) {
    -                CauseOfBlockage cause = d.canRun(this);
    -                if (cause != null)
    -                    return cause;
    +        public CauseOfBlockage getCauseOfBlockage() {
    +            if (causeOfBlockage != null) {
    +                return causeOfBlockage;
                 }
     
    -            return task.getCauseOfBlockage();
    +            // fallback for backward compatibility
    +            return getCauseOfBlockageForItem(this);
             }
     
             /*package*/ void enter(Queue q) {
    @@ -2565,7 +2682,7 @@ public class Queue extends ResourceController implements Saveable {
                     return elapsed > Math.max(d,60000L)*10;
                 } else {
                     // more than a day in the queue
    -                return TimeUnit2.MILLISECONDS.toHours(elapsed)>24;
    +                return TimeUnit.MILLISECONDS.toHours(elapsed)>24;
                 }
             }
     
    @@ -2931,4 +3048,75 @@ public class Queue extends ResourceController implements Saveable {
         public static void init(Jenkins h) {
             h.getQueue().load();
         }
    +
    +    /**
    +     * Schedule {@code Queue.save()} call for near future once items change. Ignore all changes until the time the save
    +     * takes place.
    +     *
    +     * Once queue is restored after a crash, items stages might not be accurate until the next #maintain() - this is not
    +     * a problem as the items will be reshuffled first and then scheduled during the next maintainance cycle.
    +     *
    +     * Implementation note: Queue.load() calls QueueListener hooks for every item deserialized that can hammer the persistance
    +     * on load. The problem is avoided by delaying the actual save for the time long enough for queue to load so the save
    +     * operations will collapse into one. Also, items are persisted as buildable or blocked in vast majority of cases and
    +     * those stages does not trigger the save here.
    +     */
    +    @Extension
    +    @Restricted(NoExternalUse.class)
    +    public static final class Saver extends QueueListener implements Runnable {
    +
    +        /**
    +         * All negative values will disable periodic saving.
    +         */
    +        @VisibleForTesting
    +        /*package*/ static /*final*/ int DELAY_SECONDS = SystemProperties.getInteger("hudson.model.Queue.Saver.DELAY_SECONDS", 60);
    +
    +        private final Object lock = new Object();
    +        @GuardedBy("lock")
    +        private Future<?> nextSave;
    +
    +        @Override
    +        public void onEnterWaiting(WaitingItem wi) {
    +            push();
    +        }
    +
    +        @Override
    +        public void onLeft(Queue.LeftItem li) {
    +            push();
    +        }
    +
    +        private void push() {
    +            if (DELAY_SECONDS < 0) return;
    +
    +            synchronized (lock) {
    +                // Can be done or canceled in case of a bug or external intervention - do not allow it to hang there forever
    +                if (nextSave != null && !(nextSave.isDone() || nextSave.isCancelled())) return;
    +                nextSave = Timer.get().schedule(this, DELAY_SECONDS, TimeUnit.SECONDS);
    +            }
    +        }
    +
    +        @Override
    +        public void run() {
    +            try {
    +                Jenkins j = Jenkins.getInstanceOrNull();
    +                if (j != null) {
    +                    j.getQueue().save();
    +                }
    +            } finally {
    +                synchronized (lock) {
    +                    nextSave = null;
    +                }
    +            }
    +        }
    +
    +        @VisibleForTesting @Restricted(NoExternalUse.class)
    +        /*package*/ @Nonnull Future<?> getNextSave() {
    +            synchronized (lock) {
    +                return nextSave == null
    +                        ? Futures.precomputed(null)
    +                        : nextSave
    +                ;
    +            }
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java
    index 4701bfc396e2b5447bc663aa29ba07ed14b599fe..d2a10bec27ced5c155c6b29fc9d7284b772002a2 100644
    --- a/core/src/main/java/hudson/model/Run.java
    +++ b/core/src/main/java/hudson/model/Run.java
    @@ -51,7 +51,6 @@ import hudson.cli.declarative.CLIMethod;
     import hudson.model.Descriptor.FormException;
     import hudson.model.listeners.RunListener;
     import hudson.model.listeners.SaveableListener;
    -import hudson.model.queue.Executables;
     import hudson.model.queue.SubTask;
     import hudson.search.SearchIndexBuilder;
     import hudson.security.ACL;
    @@ -60,6 +59,7 @@ import hudson.security.Permission;
     import hudson.security.PermissionGroup;
     import hudson.security.PermissionScope;
     import hudson.tasks.BuildWrapper;
    +import hudson.tasks.Fingerprinter.FingerprintAction;
     import hudson.util.FormApply;
     import hudson.util.LogTaskListener;
     import hudson.util.ProcessTree;
    @@ -74,12 +74,14 @@ import java.io.OutputStream;
     import java.io.PrintWriter;
     import java.io.RandomAccessFile;
     import java.io.Reader;
    +import java.io.Serializable;
     import java.nio.charset.Charset;
     import java.text.DateFormat;
     import java.text.SimpleDateFormat;
     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.Date;
    @@ -93,6 +95,8 @@ import java.util.Map;
     import java.util.Set;
     import java.util.logging.Level;
     import static java.util.logging.Level.*;
    +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
    +
     import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
    @@ -109,6 +113,7 @@ import jenkins.model.RunAction2;
     import jenkins.model.StandardArtifactManager;
     import jenkins.model.lazy.BuildReference;
     import jenkins.model.lazy.LazyBuildMixIn;
    +import jenkins.security.MasterToSlaveCallable;
     import jenkins.util.VirtualFile;
     import jenkins.util.io.OnMaster;
     import net.sf.json.JSONObject;
    @@ -120,8 +125,10 @@ import org.apache.commons.lang.ArrayUtils;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.HttpResponse;
    +import org.kohsuke.stapler.HttpResponses;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.Stapler;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.export.Exported;
    @@ -141,7 +148,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
      */
     @ExportedBean
     public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,RunT>>
    -        extends Actionable implements ExtensionPoint, Comparable<RunT>, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster {
    +        extends Actionable implements ExtensionPoint, Comparable<RunT>, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster, StaplerProxy {
     
         /**
          * The original {@link Queue.Item#getId()} has not yet been mapped onto the {@link Run} instance.
    @@ -774,6 +781,9 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
     
         @Override
         public String toString() {
    +        if (project == null) {
    +            return "<broken data JENKINS-45892>";
    +        }
             return project.getFullName() + " #" + number;
         }
     
    @@ -1013,11 +1023,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             return id != null ? id : Integer.toString(number);
         }
         
    -    @Override
    -    public @CheckForNull Descriptor getDescriptorByName(String className) {
    -        return Jenkins.getInstance().getDescriptorByName(className);
    -    }
    -
         /**
          * Get the root directory of this {@link Run} on the master.
          * Files related to this {@link Run} should be stored below this directory.
    @@ -1093,12 +1098,16 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
          * @return The list can be empty but never null
          */ 
         public @Nonnull List<Artifact> getArtifactsUpTo(int artifactsNumber) {
    -        ArtifactList r = new ArtifactList();
    +        SerializableArtifactList sal;
    +        VirtualFile root = getArtifactManager().root();
             try {
    -            addArtifacts(getArtifactManager().root(), "", "", r, null, artifactsNumber);
    +            sal = root.run(new AddArtifacts(root, artifactsNumber));
             } catch (IOException x) {
                 LOGGER.log(Level.WARNING, null, x);
    +            sal = new SerializableArtifactList();
             }
    +        ArtifactList r = new ArtifactList();
    +        r.updateFrom(sal);
             r.computeDisplayName();
             return r;
         }
    @@ -1112,9 +1121,25 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             return !getArtifactsUpTo(1).isEmpty();
         }
     
    -    private int addArtifacts(@Nonnull VirtualFile dir, 
    +    private static final class AddArtifacts extends MasterToSlaveCallable<SerializableArtifactList, IOException> {
    +        private static final long serialVersionUID = 1L;
    +        private final VirtualFile root;
    +        private final int artifactsNumber;
    +        AddArtifacts(VirtualFile root, int artifactsNumber) {
    +            this.root = root;
    +            this.artifactsNumber = artifactsNumber;
    +        }
    +        @Override
    +        public SerializableArtifactList call() throws IOException {
    +            SerializableArtifactList sal = new SerializableArtifactList();
    +            addArtifacts(root, "", "", sal, null, artifactsNumber);
    +            return sal;
    +        }
    +    }
    +
    +    private static int addArtifacts(@Nonnull VirtualFile dir,
                 @Nonnull String path, @Nonnull String pathHref, 
    -            @Nonnull ArtifactList r, @Nonnull Artifact parent, int upTo) throws IOException {
    +            @Nonnull SerializableArtifactList r, @CheckForNull SerializableArtifact parent, int upTo) throws IOException {
             VirtualFile[] kids = dir.list();
             Arrays.sort(kids);
     
    @@ -1125,32 +1150,32 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
                 String childHref = pathHref + Util.rawEncode(child);
                 String length = sub.isFile() ? String.valueOf(sub.length()) : "";
                 boolean collapsed = (kids.length==1 && parent!=null);
    -            Artifact a;
    +            SerializableArtifact a;
                 if (collapsed) {
                     // Collapse single items into parent node where possible:
    -                a = new Artifact(parent.getFileName() + '/' + child, childPath,
    +                a = new SerializableArtifact(parent.name + '/' + child, childPath,
                                      sub.isDirectory() ? null : childHref, length,
    -                                 parent.getTreeNodeId());
    +                                 parent.treeNodeId);
                     r.tree.put(a, r.tree.remove(parent));
                 } else {
                     // Use null href for a directory:
    -                a = new Artifact(child, childPath,
    +                a = new SerializableArtifact(child, childPath,
                                      sub.isDirectory() ? null : childHref, length,
                                      "n" + ++r.idSeq);
    -                r.tree.put(a, parent!=null ? parent.getTreeNodeId() : null);
    +                r.tree.put(a, parent!=null ? parent.treeNodeId : null);
                 }
                 if (sub.isDirectory()) {
                     n += addArtifacts(sub, childPath + '/', childHref + '/', r, a, upTo-n);
                     if (n>=upTo) break;
                 } else {
                     // Don't store collapsed path in ArrayList (for correct data in external API)
    -                r.add(collapsed ? new Artifact(child, a.relativePath, a.href, length, a.treeNodeId) : a);
    +                r.add(collapsed ? new SerializableArtifact(child, a.relativePath, a.href, length, a.treeNodeId) : a);
                     if (++n>=upTo) break;
                 }
             }
             return n;
         }
    -
    +    
         /**
          * Maximum number of artifacts to list before using switching to the tree view.
          */
    @@ -1162,6 +1187,30 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
         public static final int TREE_CUTOFF = Integer.parseInt(SystemProperties.getString("hudson.model.Run.ArtifactList.treeCutoff", "40"));
     
         // ..and then "too many"
    +    
    +    /** {@link Run.Artifact} without the implicit link to {@link Run} */
    +    private static final class SerializableArtifact implements Serializable {
    +        private static final long serialVersionUID = 1L;
    +        final String name;
    +        final String relativePath;
    +        final String href;
    +        final String length;
    +        final String treeNodeId;
    +        SerializableArtifact(String name, String relativePath, String href, String length, String treeNodeId) {
    +            this.name = name;
    +            this.relativePath = relativePath;
    +            this.href = href;
    +            this.length = length;
    +            this.treeNodeId = treeNodeId;
    +        }
    +    }
    +
    +    /** {@link Run.ArtifactList} without the implicit link to {@link Run} */
    +    private static final class SerializableArtifactList extends ArrayList<SerializableArtifact> {
    +        private static final long serialVersionUID = 1L;
    +        private LinkedHashMap<SerializableArtifact, String> tree = new LinkedHashMap<>();
    +        private int idSeq = 0;
    +    }
     
         public final class ArtifactList extends ArrayList<Artifact> {
             private static final long serialVersionUID = 1L;
    @@ -1170,7 +1219,24 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
              * Contains Artifact objects for directories and files (the ArrayList contains only files).
              */
             private LinkedHashMap<Artifact,String> tree = new LinkedHashMap<Artifact,String>();
    -        private int idSeq = 0;
    +
    +        void updateFrom(SerializableArtifactList clone) {
    +            Map<String, Artifact> artifacts = new HashMap<>(); // need to share objects between tree and list, since computeDisplayName mutates displayPath
    +            for (SerializableArtifact sa : clone) {
    +                Artifact a = new Artifact(sa);
    +                artifacts.put(a.relativePath, a);
    +                add(a);
    +            }
    +            tree = new LinkedHashMap<>();
    +            for (Map.Entry<SerializableArtifact, String> entry : clone.tree.entrySet()) {
    +                SerializableArtifact sa = entry.getKey();
    +                Artifact a = artifacts.get(sa.relativePath);
    +                if (a == null) {
    +                    a = new Artifact(sa);
    +                }
    +                tree.put(a, entry.getValue());
    +            }
    +        }
     
             public Map<Artifact,String> getTree() {
                 return tree;
    @@ -1285,6 +1351,10 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
              */
             private String length;
     
    +        Artifact(SerializableArtifact clone) {
    +            this(clone.name, clone.relativePath, clone.href, clone.length, clone.treeNodeId);
    +        }
    +
             /*package for test*/ Artifact(String name, String relativePath, String href, String len, String treeNodeId) {
                 this.name = name;
                 this.relativePath = relativePath;
    @@ -1337,6 +1407,21 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             }
         }
     
    +    /**
    +     * get the fingerprints associated with this build
    +     *
    +     * @return The fingerprints
    +     */
    +    @Nonnull
    +    @Exported(name = "fingerprint", inline = true, visibility = -1)
    +    public Collection<Fingerprint> getBuildFingerprints() {
    +        FingerprintAction fingerprintAction = getAction(FingerprintAction.class);
    +        if (fingerprintAction != null) {
    +            return fingerprintAction.getFingerprints().values();
    +        }
    +        return Collections.<Fingerprint>emptyList();
    +    }
    +    
         /**
          * Returns the log file.
          * @return The file may reference both uncompressed or compressed logs
    @@ -1390,21 +1475,12 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
         }
     
         /**
    -     * Used from <tt>console.jelly</tt> to write annotated log to the given output.
    +     * Used from {@code console.jelly} to write annotated log to the given output.
          *
          * @since 1.349
          */
         public void writeLogTo(long offset, @Nonnull XMLOutput out) throws IOException {
    -        try {
    -			getLogText().writeHtmlTo(offset,out.asWriter());
    -		} catch (IOException e) {
    -			// try to fall back to the old getLogInputStream()
    -			// mainly to support .gz compressed files
    -			// In this case, console annotation handling will be turned off.
    -			try (InputStream input = getLogInputStream()) {
    -				IOUtils.copy(input, out.asWriter());
    -			}
    -		}
    +        getLogText().writeHtmlTo(offset, out.asWriter());
         }
     
         /**
    @@ -1454,16 +1530,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             return new Api(this);
         }
     
    -    @Override
    -    public void checkPermission(@Nonnull Permission p) {
    -        getACL().checkPermission(p);
    -    }
    -
    -    @Override
    -    public boolean hasPermission(@Nonnull Permission p) {
    -        return getACL().hasPermission(p);
    -    }
    -
         @Override
         public ACL getACL() {
             // for now, don't maintain ACL per run, and do it at project level
    @@ -1500,6 +1566,10 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             
             RunListener.fireDeleted(this);
     
    +        if (artifactManager != null) {
    +            deleteArtifacts();
    +        } // for StandardArtifactManager, deleting the whole build dir suffices
    +
             synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted
             File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName());
             
    @@ -1698,6 +1768,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             if(result!=null)
                 return;     // already built.
     
    +        OutputStream logger = null;
             StreamBuildListener listener=null;
     
             runner = job;
    @@ -1716,16 +1787,20 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
                             charset = computer.getDefaultCharset();
                             this.charset = charset.name();
                         }
    -                    listener = createBuildListener(job, listener, charset);
    +                    logger = createLogger();
    +                    listener = createBuildListener(job, logger, charset);
                         listener.started(getCauses());
     
                         Authentication auth = Jenkins.getAuthentication();
                         if (!auth.equals(ACL.SYSTEM)) {
    -                        String name = auth.getName();
    +                        String id = auth.getName();
                             if (!auth.equals(Jenkins.ANONYMOUS)) {
    -                            name = ModelHyperlinkNote.encodeTo(User.get(name));
    +                            final User usr = User.getById(id, false);
    +                            if (usr != null) { // Encode user hyperlink for existing users
    +                                id = ModelHyperlinkNote.encodeTo(usr);
    +                            }
                             }
    -                        listener.getLogger().println(Messages.Run_running_as_(name));
    +                        listener.getLogger().println(Messages.Run_running_as_(id));
                         }
     
                         RunListener.fireStarted(this,listener);
    @@ -1798,23 +1873,32 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
                 try {
                     getParent().logRotate();
                 } catch (Exception e) {
    -		LOGGER.log(Level.SEVERE, "Failed to rotate log",e);
    -	    }
    +                LOGGER.log(Level.SEVERE, "Failed to rotate log",e);
    +            }
             } finally {
                 onEndBuilding();
    +            if (logger != null) {
    +                try {
    +                    logger.close();
    +                } catch (IOException x) {
    +                    LOGGER.log(Level.WARNING, "failed to close log for " + Run.this, x);
    +                }
    +            }
             }
         }
     
    -    private StreamBuildListener createBuildListener(@Nonnull RunExecution job, StreamBuildListener listener, Charset charset) throws IOException, InterruptedException {
    +    private OutputStream createLogger() throws IOException {
             // don't do buffering so that what's written to the listener
             // gets reflected to the file immediately, which can then be
             // served to the browser immediately
    -        OutputStream logger;
             try {
    -            logger = Files.newOutputStream(getLogFile().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
    +            return Files.newOutputStream(getLogFile().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
             } catch (InvalidPathException e) {
                 throw new IOException(e);
             }
    +    }
    +
    +    private StreamBuildListener createBuildListener(@Nonnull RunExecution job, OutputStream logger, Charset charset) throws IOException, InterruptedException {
             RunT build = job.getBuild();
     
             // Global log filters
    @@ -1830,8 +1914,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
                 }
             }
     
    -        listener = new StreamBuildListener(logger,charset);
    -        return listener;
    +        return new StreamBuildListener(logger,charset);
         }
     
         /**
    @@ -1928,6 +2011,19 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml"));
         }
     
    +    private Object writeReplace() {
    +        return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this));
    +    }
    +    private static class Replacer {
    +        private final String id;
    +        Replacer(Run<?, ?> r) {
    +            id = r.getExternalizableId();
    +        }
    +        private Object readResolve() {
    +            return fromExternalizableId(id);
    +        }
    +    }
    +
         /**
          * Gets the log of the build as a string.
          * @return Returns the log or an empty string if it has not been found
    @@ -2128,7 +2224,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
          */
         public void doConsoleText(StaplerRequest req, StaplerResponse rsp) throws IOException {
             rsp.setContentType("text/plain;charset=UTF-8");
    -        ;
             try (InputStream input = getLogInputStream();
                  OutputStream os = rsp.getCompressedOutputStream(req);
                  PlainTextConsoleOutputStream out = new PlainTextConsoleOutputStream(os)) {
    @@ -2277,6 +2372,12 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             for (EnvironmentContributor ec : EnvironmentContributor.all().reverseView())
                 ec.buildEnvironmentFor(this,env,listener);
     
    +        if (!(this instanceof AbstractBuild)) {
    +            for (EnvironmentContributingAction a : getActions(EnvironmentContributingAction.class)) {
    +                a.buildEnvironment(this, env);
    +            }
    +        } // else for compatibility reasons, handled in override after buildEnvironments
    +
             return env;
         }
     
    @@ -2319,7 +2420,10 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             } catch (NumberFormatException x) {
                 throw new IllegalArgumentException(x);
             }
    -        Jenkins j = Jenkins.getInstance();
    +        Jenkins j = Jenkins.getInstanceOrNull();
    +        if (j == null) {
    +            return null;
    +        }
             Job<?,?> job = j.getItemByFullName(jobName, Job.class);
             if (job == null) {
                 return null;
    @@ -2470,6 +2574,26 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
             return returnedResult;
         }
     
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            // This is a bit weird, but while the Run's PermissionScope does not have READ, delegate to the parent
    +            if (!getParent().hasPermission(Item.DISCOVER)) {
    +                return null;
    +            }
    +            getParent().checkPermission(Item.READ);
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(Run.class.getName() + ".skipPermissionCheck");
    +
    +
         public static class RedirectUp {
             public void doDynamic(StaplerResponse rsp) throws IOException {
                 // Compromise to handle both browsers (auto-redirect) and programmatic access
    diff --git a/core/src/main/java/hudson/model/RunParameterDefinition.java b/core/src/main/java/hudson/model/RunParameterDefinition.java
    index da1e7865e62734ae184fad71e5c0b2b01a53d2df..8f725193f22439c545695924a2445d00664763fd 100644
    --- a/core/src/main/java/hudson/model/RunParameterDefinition.java
    +++ b/core/src/main/java/hudson/model/RunParameterDefinition.java
    @@ -92,7 +92,7 @@ public class RunParameterDefinition extends SimpleParameterDefinition {
         public ParameterDefinition copyWithDefaultValue(ParameterValue defaultValue) {
             if (defaultValue instanceof RunParameterValue) {
                 RunParameterValue value = (RunParameterValue) defaultValue;
    -            return new RunParameterDefinition(getName(), value.getRunId(), getDescription(), getFilter());
    +            return new RunParameterDefinition(getName(), getProjectName(), value.getRunId(), getDescription(), getFilter());
             } else {
                 return this;
             }
    diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java
    index 7d3721fbbdf855e0df1e91275456150874c9c8a9..c3cad4eea7390fbf173981619bdef4777eb5c53b 100644
    --- a/core/src/main/java/hudson/model/Slave.java
    +++ b/core/src/main/java/hudson/model/Slave.java
    @@ -26,15 +26,16 @@ package hudson.model;
     
     import com.google.common.collect.ImmutableSet;
     import hudson.DescriptorExtensionList;
    +import hudson.EnvVars;
     import hudson.FilePath;
     import hudson.Launcher;
     import hudson.Launcher.RemoteLauncher;
     import hudson.Util;
    +import hudson.cli.CLI;
     import hudson.model.Descriptor.FormException;
     import hudson.remoting.Callable;
     import hudson.remoting.Channel;
     import hudson.remoting.Which;
    -import hudson.slaves.CommandLauncher;
     import hudson.slaves.ComputerLauncher;
     import hudson.slaves.DumbSlave;
     import hudson.slaves.JNLPLauncher;
    @@ -47,6 +48,7 @@ import hudson.util.ClockDifference;
     import hudson.util.DescribableList;
     import hudson.util.FormValidation;
     import java.io.File;
    +import java.io.FileNotFoundException;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.Serializable;
    @@ -57,6 +59,8 @@ import java.util.ArrayList;
     import java.util.Collection;
     import java.util.List;
     import java.util.Set;
    +import java.util.jar.JarFile;
    +import java.util.jar.Manifest;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
    @@ -80,7 +84,7 @@ import org.kohsuke.stapler.StaplerResponse;
      * Information about a Hudson agent node.
      *
      * <p>
    - * Ideally this would have been in the <tt>hudson.slaves</tt> package,
    + * Ideally this would have been in the {@code hudson.slaves} package,
      * but for compatibility reasons, it can't.
      *
      * <p>
    @@ -142,8 +146,8 @@ public abstract class Slave extends Node implements Serializable {
          */
         private String label="";
     
    -    private /*almost final*/ DescribableList<NodeProperty<?>,NodePropertyDescriptor> nodeProperties = 
    -                                    new DescribableList<NodeProperty<?>,NodePropertyDescriptor>(Jenkins.getInstance().getNodesObject());
    +    private /*almost final*/ DescribableList<NodeProperty<?>,NodePropertyDescriptor> nodeProperties =
    +            new DescribableList<>(this);
     
         /**
          * Lazily computed set of labels from {@link #label}.
    @@ -176,9 +180,10 @@ public abstract class Slave extends Node implements Serializable {
         }
     
         /**
    -     * @deprecated as of 1.XXX
    +     * @deprecated as of 2.2
          *      Use {@link #Slave(String, String, ComputerLauncher)} and set the rest through setters.
          */
    +    @Deprecated
         public Slave(@Nonnull String name, String nodeDescription, String remoteFS, int numExecutors,
                      Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, List<? extends NodeProperty<?>> nodeProperties) throws FormException, IOException {
             this.name = name;
    @@ -225,7 +230,17 @@ public abstract class Slave extends Node implements Serializable {
         }
     
         public ComputerLauncher getLauncher() {
    -        return launcher == null ? new JNLPLauncher() : launcher;
    +        if (launcher == null && !StringUtils.isEmpty(agentCommand)) {
    +            try {
    +                launcher = (ComputerLauncher) Jenkins.getInstance().getPluginManager().uberClassLoader.loadClass("hudson.slaves.CommandLauncher").getConstructor(String.class, EnvVars.class).newInstance(agentCommand, null);
    +                agentCommand = null;
    +                save();
    +            } catch (Exception x) {
    +                LOGGER.log(Level.WARNING, "could not update historical agentCommand setting to CommandLauncher", x);
    +            }
    +        }
    +        // Default launcher does not use Work Directory
    +        return launcher == null ? new JNLPLauncher(false) : launcher;
         }
     
         public void setLauncher(ComputerLauncher launcher) {
    @@ -388,20 +403,58 @@ public abstract class Slave extends Node implements Serializable {
                     throw new MalformedURLException("The specified file path " + fileName + " is not allowed due to security reasons");
                 }
                 
    -            if (name.equals("hudson-cli.jar"))  {
    -                name="jenkins-cli.jar";
    -            } else if (name.equals("slave.jar") || name.equals("remoting.jar")) {
    -                name = "lib/" + Which.jarFile(Channel.class).getName();
    +            if (name.equals("hudson-cli.jar") || name.equals("jenkins-cli.jar"))  {
    +                File cliJar = Which.jarFile(CLI.class);
    +                if (cliJar.isFile()) {
    +                    name = "jenkins-cli.jar";
    +                } else {
    +                    URL res = findExecutableJar(cliJar, CLI.class);
    +                    if (res != null) {
    +                        return res;
    +                    }
    +                }
    +            } else if (name.equals("agent.jar") || name.equals("slave.jar") || name.equals("remoting.jar")) {
    +                File remotingJar = Which.jarFile(hudson.remoting.Launcher.class);
    +                if (remotingJar.isFile()) {
    +                    name = "lib/" + remotingJar.getName();
    +                } else {
    +                    URL res = findExecutableJar(remotingJar, hudson.remoting.Launcher.class);
    +                    if (res != null) {
    +                        return res;
    +                    }
    +                }
                 }
                 
                 URL res = Jenkins.getInstance().servletContext.getResource("/WEB-INF/" + name);
                 if(res==null) {
    -                // during the development this path doesn't have the files.
    -                res = new URL(new File(".").getAbsoluteFile().toURI().toURL(),"target/jenkins/WEB-INF/"+name);
    +                throw new FileNotFoundException(name); // giving up
    +            } else {
    +                LOGGER.log(Level.FINE, "found {0}", res);
                 }
                 return res;
             }
     
    +        /** Useful for {@code JenkinsRule.createSlave}, {@code hudson-dev:run}, etc. */
    +        private @CheckForNull URL findExecutableJar(File notActuallyJAR, Class<?> mainClass) throws IOException {
    +            if (notActuallyJAR.getName().equals("classes")) {
    +                File[] siblings = notActuallyJAR.getParentFile().listFiles();
    +                if (siblings != null) {
    +                    for (File actualJar : siblings) {
    +                        if (actualJar.getName().endsWith(".jar")) {
    +                            try (JarFile jf = new JarFile(actualJar, false)) {
    +                                Manifest mf = jf.getManifest();
    +                                if (mf != null && mainClass.getName().equals(mf.getMainAttributes().getValue("Main-Class"))) {
    +                                    LOGGER.log(Level.FINE, "found {0}", actualJar);
    +                                    return actualJar.toURI().toURL();
    +                                }
    +                            }
    +                        }
    +                    }
    +                }
    +            }
    +            return null;
    +        }
    +
             public byte[] readFully() throws IOException {
                 try (InputStream in = connect().getInputStream()) {
                     return IOUtils.toByteArray(in);
    @@ -441,19 +494,19 @@ public abstract class Slave extends Node implements Serializable {
                 // RemoteLauncher requires an active Channel instance to operate correctly
                 final Channel channel = c.getChannel();
                 if (channel == null) { 
    -                reportLauncerCreateError("The agent has not been fully initialized yet",
    +                reportLauncherCreateError("The agent has not been fully initialized yet",
                                              "No remoting channel to the agent OR it has not been fully initialized yet", listener);
                     return new Launcher.DummyLauncher(listener);
                 }
                 if (channel.isClosingOrClosed()) {
    -                reportLauncerCreateError("The agent is being disconnected",
    +                reportLauncherCreateError("The agent is being disconnected",
                                              "Remoting channel is either in the process of closing down or has closed down", listener);
                     return new Launcher.DummyLauncher(listener);
                 }
                 final Boolean isUnix = c.isUnix();
                 if (isUnix == null) {
                     // isUnix is always set when the channel is not null, so it should never happen
    -                reportLauncerCreateError("The agent has not been fully initialized yet",
    +                reportLauncherCreateError("The agent has not been fully initialized yet",
                                              "Cannot determing if the agent is a Unix one, the System status request has not completed yet. " +
                                              "It is an invalid channel state, please report a bug to Jenkins if you see it.", 
                                              listener);
    @@ -464,7 +517,7 @@ public abstract class Slave extends Node implements Serializable {
             }
         }
         
    -    private void reportLauncerCreateError(@Nonnull String humanReadableMsg, @CheckForNull String exceptionDetails, @Nonnull TaskListener listener) {
    +    private void reportLauncherCreateError(@Nonnull String humanReadableMsg, @CheckForNull String exceptionDetails, @Nonnull TaskListener listener) {
             String message = "Issue with creating launcher for agent " + name + ". " + humanReadableMsg;
             listener.error(message);
             if (LOGGER.isLoggable(Level.WARNING)) {
    @@ -477,7 +530,12 @@ public abstract class Slave extends Node implements Serializable {
     
         /**
          * Gets the corresponding computer object.
    +     *
    +     * @return
    +     *      this method can return null if there's no {@link Computer} object for this node,
    +     *      such as when this node has no executors at all.
          */
    +    @CheckForNull
         public SlaveComputer getComputer() {
             return (SlaveComputer)toComputer();
         }
    @@ -501,14 +559,8 @@ public abstract class Slave extends Node implements Serializable {
          * Invoked by XStream when this object is read into memory.
          */
         protected Object readResolve() {
    -        // convert the old format to the new one
    -        if (launcher == null) {
    -            launcher = (agentCommand == null || agentCommand.trim().length() == 0)
    -                    ? new JNLPLauncher()
    -                    : new CommandLauncher(agentCommand);
    -        }
             if(nodeProperties==null)
    -            nodeProperties = new DescribableList<NodeProperty<?>,NodePropertyDescriptor>(Jenkins.getInstance().getNodesObject());
    +            nodeProperties = new DescribableList<>(this);
             return this;
         }
     
    @@ -673,5 +725,5 @@ public abstract class Slave extends Node implements Serializable {
         /**
          * Provides a collection of file names, which are accessible via /jnlpJars link.
          */
    -    private static final Set<String> ALLOWED_JNLPJARS_FILES = ImmutableSet.of("slave.jar", "remoting.jar", "jenkins-cli.jar", "hudson-cli.jar");
    +    private static final Set<String> ALLOWED_JNLPJARS_FILES = ImmutableSet.of("agent.jar", "slave.jar", "remoting.jar", "jenkins-cli.jar", "hudson-cli.jar");
     }
    diff --git a/core/src/main/java/hudson/model/StreamBuildListener.java b/core/src/main/java/hudson/model/StreamBuildListener.java
    index aa6bff2521bccb0cbd29a0344e5bf9b3a76e2021..226f06ba26aaa6c39b320a6d89a71f9d660382a8 100644
    --- a/core/src/main/java/hudson/model/StreamBuildListener.java
    +++ b/core/src/main/java/hudson/model/StreamBuildListener.java
    @@ -30,7 +30,6 @@ import java.io.IOException;
     import java.io.OutputStream;
     import java.io.PrintStream;
     import java.nio.charset.Charset;
    -import java.util.List;
     
     /**
      * {@link BuildListener} that writes to an {@link OutputStream}.
    @@ -66,19 +65,5 @@ public class StreamBuildListener extends StreamTaskListener implements BuildList
             super(w,charset);
         }
     
    -    public void started(List<Cause> causes) {
    -        PrintStream l = getLogger();
    -        if (causes==null || causes.isEmpty())
    -            l.println("Started");
    -        else for (Cause cause : causes) {
    -            // TODO elide duplicates as per CauseAction.getCauseCounts (used in summary.jelly)
    -            cause.print(this);
    -        }
    -    }
    -
    -    public void finished(Result result) {
    -        getLogger().println("Finished: "+result);
    -    }
    -
         private static final long serialVersionUID = 1L;
     }
    diff --git a/core/src/main/java/hudson/model/StringParameterDefinition.java b/core/src/main/java/hudson/model/StringParameterDefinition.java
    index c38bffc5a5282ddba4f8f4483bd8c789753c2f09..3573199a01e4bd596d6a8d2e32569cdc361540b2 100644
    --- a/core/src/main/java/hudson/model/StringParameterDefinition.java
    +++ b/core/src/main/java/hudson/model/StringParameterDefinition.java
    @@ -24,8 +24,12 @@
     package hudson.model;
     
     import hudson.Extension;
    +import hudson.Util;
    +import javax.annotation.Nonnull;
     import net.sf.json.JSONObject;
     import org.jenkinsci.Symbol;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.DoNotUse;
     import org.kohsuke.stapler.DataBoundConstructor;
     import org.kohsuke.stapler.StaplerRequest;
     
    @@ -35,15 +39,22 @@ import org.kohsuke.stapler.StaplerRequest;
     public class StringParameterDefinition extends SimpleParameterDefinition {
     
         private String defaultValue;
    +    private final boolean trim;
     
         @DataBoundConstructor
    -    public StringParameterDefinition(String name, String defaultValue, String description) {
    +    public StringParameterDefinition(String name, String defaultValue, String description, boolean trim) {
             super(name, description);
             this.defaultValue = defaultValue;
    +        this.trim = trim;
         }
     
    +    @Nonnull
    +    public StringParameterDefinition(String name, String defaultValue, String description) {
    +        this(name, defaultValue, description, false);
    +    }
    +    
         public StringParameterDefinition(String name, String defaultValue) {
    -        this(name, defaultValue, null);
    +        this(name, defaultValue, null, false);
         }
     
         @Override
    @@ -60,14 +71,40 @@ public class StringParameterDefinition extends SimpleParameterDefinition {
             return defaultValue;
         }
     
    +    /**
    +     * 
    +     * @return original or trimmed defaultValue (depending on trim)
    +     */
    +    @Restricted(DoNotUse.class) // Jelly
    +    public String getDefaultValue4Build() {
    +        if (isTrim()) {
    +            return Util.fixNull(defaultValue).trim();
    +        }
    +        return defaultValue;
    +    }
    +    
         public void setDefaultValue(String defaultValue) {
             this.defaultValue = defaultValue;
         }
    +
    +    /**
    +     * 
    +     * @return trim - {@code true}, if trim options has been selected, else return {@code false}.
    +     *      Trimming will happen when creating {@link StringParameterValue}s,
    +     *      the value in the config will not be changed.
    +     * @since 2.90
    +     */
    +    public boolean isTrim() {
    +        return trim;
    +    }
         
         @Override
         public StringParameterValue getDefaultParameterValue() {
    -        StringParameterValue v = new StringParameterValue(getName(), defaultValue, getDescription());
    -        return v;
    +        StringParameterValue value = new StringParameterValue(getName(), defaultValue, getDescription());
    +        if (isTrim()) {
    +            value.doTrim();
    +        }
    +        return value;
         }
     
         @Extension @Symbol({"string","stringParam"})
    @@ -86,11 +123,18 @@ public class StringParameterDefinition extends SimpleParameterDefinition {
         @Override
         public ParameterValue createValue(StaplerRequest req, JSONObject jo) {
             StringParameterValue value = req.bindJSON(StringParameterValue.class, jo);
    +        if (isTrim() && value!=null) {
    +            value.doTrim();
    +        }
             value.setDescription(getDescription());
             return value;
         }
     
    -    public ParameterValue createValue(String value) {
    -        return new StringParameterValue(getName(), value, getDescription());
    +    public ParameterValue createValue(String str) {
    +        StringParameterValue value = new StringParameterValue(getName(), str, getDescription());
    +        if (isTrim() && value!=null) {
    +            value.doTrim();
    +        }
    +        return value;
         }
     }
    diff --git a/core/src/main/java/hudson/model/StringParameterValue.java b/core/src/main/java/hudson/model/StringParameterValue.java
    index f841862237cf10e458dbba79e3134a1bc2d3a8a5..a1bf1516e8c0265a74869ddc3f869f828e3f2068 100644
    --- a/core/src/main/java/hudson/model/StringParameterValue.java
    +++ b/core/src/main/java/hudson/model/StringParameterValue.java
    @@ -30,13 +30,16 @@ import org.kohsuke.stapler.export.Exported;
     import java.util.Locale;
     
     import hudson.util.VariableResolver;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
      * {@link ParameterValue} created from {@link StringParameterDefinition}.
      */
     public class StringParameterValue extends ParameterValue {
         @Exported(visibility=4)
    -    public final String value;
    +    @Restricted(NoExternalUse.class)
    +    public String value;
     
         @DataBoundConstructor
         public StringParameterValue(String name, String value) {
    @@ -70,6 +73,16 @@ public class StringParameterValue extends ParameterValue {
         public Object getValue() {
             return value;
         }
    +     
    +    /**
    +     * Trimming for value
    +     * @since 2.90
    +     */
    +    public void doTrim() {
    +        if (value != null) {
    +           value = value.trim(); 
    +        } 
    +    }
     
         @Override
     	public int hashCode() {
    diff --git a/core/src/main/java/hudson/model/TaskListener.java b/core/src/main/java/hudson/model/TaskListener.java
    index d2f7457e06ad5c01a9af23561ca362a63d11ca63..e5e27f56c5c8cb92189f6161a1aafed79dd2bc83 100644
    --- a/core/src/main/java/hudson/model/TaskListener.java
    +++ b/core/src/main/java/hudson/model/TaskListener.java
    @@ -25,15 +25,22 @@ package hudson.model;
     
     import hudson.console.ConsoleNote;
     import hudson.console.HyperlinkNote;
    -import hudson.util.AbstractTaskListener;
    +import hudson.remoting.Channel;
     import hudson.util.NullStream;
     import hudson.util.StreamTaskListener;
     
     import java.io.IOException;
    +import java.io.OutputStreamWriter;
     import java.io.PrintStream;
     import java.io.PrintWriter;
     import java.io.Serializable;
    +import java.nio.charset.Charset;
    +import java.nio.charset.StandardCharsets;
     import java.util.Formatter;
    +import javax.annotation.Nonnull;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +import org.kohsuke.accmod.restrictions.ProtectedExternally;
     
     /**
      * Receives events that happen during some lengthy operation
    @@ -53,26 +60,50 @@ import java.util.Formatter;
      *
      * <p>
      * {@link StreamTaskListener} is the most typical implementation of this interface.
    - * All the {@link TaskListener} implementations passed to plugins from Hudson core are remotable.
      *
    - * @see AbstractTaskListener
    + * <p>
    + * Implementations are generally expected to be remotable via {@link Channel}.
    + *
      * @author Kohsuke Kawaguchi
      */
     public interface TaskListener extends Serializable {
         /**
          * This writer will receive the output of the build
    -     *
    -     * @return
    -     *      must be non-null.
          */
    +    @Nonnull
         PrintStream getLogger();
     
    +    /**
    +     * A charset to use for methods returning {@link PrintWriter}.
    +     * Should match that used to construct {@link #getLogger}.
    +     * @return by default, UTF-8
    +     */
    +    @Restricted(ProtectedExternally.class)
    +    @Nonnull
    +    default Charset getCharset() {
    +        return StandardCharsets.UTF_8;
    +    }
    +
    +    @Restricted(NoExternalUse.class) // TODO Java 9 make private
    +    default PrintWriter _error(String prefix, String msg) {
    +        PrintStream out = getLogger();
    +        out.print(prefix);
    +        out.println(msg);
    +
    +        // annotate(new HudsonExceptionNote()) if and when this is made to do something
    +        Charset charset = getCharset();
    +        return new PrintWriter(charset != null ? new OutputStreamWriter(out, charset) : new OutputStreamWriter(out), true);
    +    }
    +
         /**
          * Annotates the current position in the output log by using the given annotation.
          * If the implementation doesn't support annotated output log, this method might be no-op.
          * @since 1.349
          */
    -    void annotate(ConsoleNote ann) throws IOException;
    +    @SuppressWarnings("rawtypes")
    +    default void annotate(ConsoleNote ann) throws IOException {
    +        ann.encodeTo(getLogger());
    +    }
     
         /**
          * Places a {@link HyperlinkNote} on the given text.
    @@ -80,33 +111,48 @@ public interface TaskListener extends Serializable {
          * @param url
          *      If this starts with '/', it's interpreted as a path within the context path.
          */
    -    void hyperlink(String url, String text) throws IOException;
    +    default void hyperlink(String url, String text) throws IOException {
    +        annotate(new HyperlinkNote(url, text.length()));
    +        getLogger().print(text);
    +    }
     
         /**
          * An error in the build.
          *
          * @return
    -     *      A writer to receive details of the error. Not null.
    +     *      A writer to receive details of the error.
          */
    -    PrintWriter error(String msg);
    +    @Nonnull
    +    default PrintWriter error(String msg) {
    +        return _error("ERROR: ", msg);
    +    }
     
         /**
          * {@link Formatter#format(String, Object[])} version of {@link #error(String)}.
          */
    -    PrintWriter error(String format, Object... args);
    +    @Nonnull
    +    default PrintWriter error(String format, Object... args) {
    +        return error(String.format(format,args));
    +    }
     
         /**
          * A fatal error in the build.
          *
          * @return
    -     *      A writer to receive details of the error. Not null.
    +     *      A writer to receive details of the error.
          */
    -    PrintWriter fatalError(String msg);
    +    @Nonnull
    +    default PrintWriter fatalError(String msg) {
    +        return _error("FATAL: ", msg);
    +    }
     
         /**
          * {@link Formatter#format(String, Object[])} version of {@link #fatalError(String)}.
          */
    -    PrintWriter fatalError(String format, Object... args);
    +    @Nonnull
    +    default PrintWriter fatalError(String format, Object... args) {
    +        return fatalError(String.format(format, args));
    +    }
     
         /**
          * {@link TaskListener} that discards the output.
    diff --git a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java
    index 32aee86d1c638d9424bb78ab73fb48a4a046f8e8..91488a22cc72f833f45322dd68704dfb0f4e473f 100644
    --- a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java
    +++ b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java
    @@ -125,7 +125,7 @@ public abstract class TopLevelItemDescriptor extends Descriptor<TopLevelItem> im
          *
          * <p>
          * Used as the caption when the user chooses what item type to create.
    -     * The descriptor implementation also needs to have <tt>newInstanceDetail.jelly</tt>
    +     * The descriptor implementation also needs to have {@code newInstanceDetail.jelly}
          * script, which will be used to render the text below the caption
          * that explains the item type.
          */
    @@ -135,10 +135,11 @@ public abstract class TopLevelItemDescriptor extends Descriptor<TopLevelItem> im
         }
     
         /**
    -     * A description of this kind of item type. This description can contain HTML code but it is recommend to use text plain
    -     * in order to avoid how it should be represented.
    +     * A description of this kind of item type. This description can contain HTML code but it is recommended that
    +     * you use plain text in order to be consistent with the rest of Jenkins.
          *
    -     * This method should be called in a thread where Stapler is associated, but it will return an empty string.
    +     * This method should be called from a thread where Stapler is handling an HTTP request, otherwise it will
    +     * return an empty string.
          *
          * @return A string, by default the value from newInstanceDetail view is taken.
          *
    diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java
    index cf6cb0d684898cc85095cf7fcc1c3fef1b0bd58a..fffd08d45ed026e69bcad39ddf4c269894a5b579 100644
    --- a/core/src/main/java/hudson/model/UpdateCenter.java
    +++ b/core/src/main/java/hudson/model/UpdateCenter.java
    @@ -23,6 +23,7 @@
      */
     package hudson.model;
     
    +import com.google.common.annotations.VisibleForTesting;
     import hudson.BulkChange;
     import hudson.Extension;
     import hudson.ExtensionPoint;
    @@ -31,12 +32,16 @@ import hudson.PluginManager;
     import hudson.PluginWrapper;
     import hudson.ProxyConfiguration;
     import hudson.security.ACLContext;
    +import hudson.util.VersionNumber;
     import java.nio.file.Files;
     import java.nio.file.InvalidPathException;
    +
    +import jenkins.security.stapler.StaplerDispatchable;
     import jenkins.util.SystemProperties;
     import hudson.Util;
     import hudson.XmlFile;
     import static hudson.init.InitMilestone.PLUGINS_STARTED;
    +import static java.util.logging.Level.INFO;
     import static java.util.logging.Level.WARNING;
     
     import hudson.init.Initializer;
    @@ -51,7 +56,6 @@ import hudson.util.DaemonThreadFactory;
     import hudson.util.FormValidation;
     import hudson.util.HttpResponses;
     import hudson.util.NamingThreadFactory;
    -import hudson.util.IOException2;
     import hudson.util.PersistedList;
     import hudson.util.XStream2;
     import jenkins.MissingDependencyException;
    @@ -59,6 +63,7 @@ import jenkins.RestartRequiredException;
     import jenkins.install.InstallUtil;
     import jenkins.model.Jenkins;
     import jenkins.util.io.OnMaster;
    +import jenkins.util.java.JavaUtils;
     import net.sf.json.JSONObject;
     
     import org.acegisecurity.Authentication;
    @@ -70,6 +75,7 @@ import org.jenkinsci.Symbol;
     import org.jvnet.localizer.Localizable;
     import org.kohsuke.accmod.restrictions.DoNotUse;
     import org.kohsuke.stapler.HttpResponse;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     
    @@ -146,9 +152,10 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
      * @since 1.220
      */
     @ExportedBean
    -public class UpdateCenter extends AbstractModelObject implements Saveable, OnMaster {
    +public class UpdateCenter extends AbstractModelObject implements Saveable, OnMaster, StaplerProxy {
     
    -    private static final String UPDATE_CENTER_URL = SystemProperties.getString(UpdateCenter.class.getName()+".updateCenterUrl","http://updates.jenkins-ci.org/");
    +    private static final Logger LOGGER;
    +    private static final String UPDATE_CENTER_URL;
     
         /**
          * Read timeout when downloading plugins, defaults to 1 minute
    @@ -202,6 +209,25 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
     
         private boolean requiresRestart;
     
    +    static {
    +        Logger logger = Logger.getLogger(UpdateCenter.class.getName());
    +        LOGGER = logger;
    +        String ucOverride = SystemProperties.getString(UpdateCenter.class.getName()+".updateCenterUrl");
    +        if (ucOverride != null) {
    +            logger.log(Level.INFO, "Using a custom update center defined by the system property: {0}", ucOverride);
    +            UPDATE_CENTER_URL = ucOverride;
    +        } else if (JavaUtils.isRunningWithJava8OrBelow()) {
    +            UPDATE_CENTER_URL = "https://updates.jenkins.io/";
    +        } else {
    +            //TODO: Rollback the default for Java 11 when it goes to GA
    +            String experimentalJava11UC = "https://updates.jenkins.io/temporary-experimental-java11/";
    +            logger.log(Level.WARNING, "Running Jenkins with Java {0} which is available in the preview mode only. " +
    +                    "A custom experimental update center will be used: {1}",
    +                    new Object[] {System.getProperty("java.specification.version"), experimentalJava11UC});
    +            UPDATE_CENTER_URL = experimentalJava11UC;
    +        }
    +    }
    +
         /**
          * Simple connection status enum.
          */
    @@ -292,7 +318,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
         }
     
         public Api getApi() {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             return new Api(this);
         }
     
    @@ -317,6 +342,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          *      can be empty but never null. Oldest entries first.
          */
         @Exported
    +    @StaplerDispatchable
         public List<UpdateCenterJob> getJobs() {
             synchronized (jobs) {
                 return new ArrayList<UpdateCenterJob>(jobs);
    @@ -364,7 +390,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @Restricted(DoNotUse.class)
         public HttpResponse doConnectionStatus(StaplerRequest request) {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             try {
                 String siteId = request.getParameter("siteId");
                 if (siteId == null) {
    @@ -417,7 +442,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @Restricted(DoNotUse.class) // WebOnly
         public HttpResponse doIncompleteInstallStatus() {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             try {
             Map<String,String> jobs = InstallUtil.getPersistedInstallStatus();
             if(jobs == null) {
    @@ -467,7 +491,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @Restricted(DoNotUse.class)
         public HttpResponse doInstallStatus(StaplerRequest request) {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             try {
                 String correlationId = request.getParameter("correlationId");
                 Map<String,Object> response = new HashMap<>();
    @@ -520,6 +543,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          * @return
          *      can be empty but never null.
          */
    +    @StaplerDispatchable // referenced by _api.jelly
         public PersistedList<UpdateSite> getSites() {
             return sites;
         }
    @@ -576,7 +600,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
         }
     
         /**
    -     * Gets the {@link UpdateSite} from which we receive updates for <tt>jenkins.war</tt>.
    +     * Gets the {@link UpdateSite} from which we receive updates for {@code jenkins.war}.
          *
          * @return
          *      {@code null} if no such update center is provided.
    @@ -615,12 +639,29 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
             return null;
         }
     
    +    /**
    +     * Gets the plugin with the given name from the first {@link UpdateSite} to contain it.
    +     * @return Discovered {@link Plugin}. {@code null} if it cannot be found
    +     */
    +    public @CheckForNull Plugin getPlugin(String artifactId, @CheckForNull VersionNumber minVersion) {
    +        if (minVersion == null) {
    +            return getPlugin(artifactId);
    +        }
    +        for (UpdateSite s : sites) {
    +            Plugin p = s.getPlugin(artifactId);
    +            if (p!=null) {
    +                if (minVersion.isNewerThan(new VersionNumber(p.version))) continue;
    +                return p;
    +            }
    +        }
    +        return null;
    +    }
    +
         /**
          * Schedules a Jenkins upgrade.
          */
         @RequirePOST
         public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             HudsonUpgradeJob job = new HudsonUpgradeJob(getCoreSource(), Jenkins.getAuthentication());
             if(!Lifecycle.get().canRewriteHudsonWar()) {
                 sendError("Jenkins upgrade not supported in this running mode");
    @@ -639,7 +680,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @RequirePOST
         public HttpResponse doInvalidateData() {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             for (UpdateSite site : sites) {
                 site.doInvalidateData();
             }
    @@ -655,7 +695,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
         public void doSafeRestart(StaplerRequest request, StaplerResponse response) throws IOException, ServletException {
             synchronized (jobs) {
                 if (!isRestartScheduled()) {
    -                Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
                     addJob(new RestartJenkinsJob(getCoreSource()));
                     LOGGER.info("Scheduling Jenkins reboot");
                 }
    @@ -726,7 +765,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @RequirePOST
         public void doDowngrade(StaplerResponse rsp) throws IOException, ServletException {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             if(!isDowngradable()) {
                 sendError("Jenkins downgrade is not possible, probably backup does not exist");
                 return;
    @@ -743,7 +781,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @RequirePOST
         public void doRestart(StaplerResponse rsp) throws IOException, ServletException {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             HudsonDowngradeJob job = new HudsonDowngradeJob(getCoreSource(), Jenkins.getAuthentication());
             LOGGER.info("Scheduling the core downgrade");
     
    @@ -982,7 +1019,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
             return results;
         }
     
    -
         /**
          * {@link AdministrativeMonitor} that checks if there's Jenkins update.
          */
    @@ -1107,12 +1143,15 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
              */
             public File download(DownloadJob job, URL src) throws IOException {
                 MessageDigest sha1 = null;
    +            MessageDigest sha256 = null;
    +            MessageDigest sha512 = null;
                 try {
    +                // Java spec says SHA-1 and SHA-256 exist, and SHA-512 might not, so one try/catch block should be fine
                     sha1 = MessageDigest.getInstance("SHA-1");
    -            } catch (NoSuchAlgorithmException ignored) {
    -                // Irrelevant as the Java spec says SHA-1 must exist. Still, if this fails
    -                // the DownloadJob will just have computedSha1 = null and that is expected
    -                // to be handled by caller
    +                sha256 = MessageDigest.getInstance("SHA-256");
    +                sha512 = MessageDigest.getInstance("SHA-512");
    +            } catch (NoSuchAlgorithmException nsa) {
    +                LOGGER.log(Level.WARNING, "Failed to instantiate message digest algorithm, may only have weak or no verification of downloaded file", nsa);
                 }
     
                 URLConnection con = null;
    @@ -1135,7 +1174,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
                     String oldName = t.getName();
                     t.setName(oldName + ": " + src);
                     try (OutputStream _out = Files.newOutputStream(tmp.toPath());
    -                     OutputStream out = sha1 != null ? new DigestOutputStream(_out, sha1) : _out;
    +                     OutputStream out =
    +                             sha1 != null ? new DigestOutputStream(
    +                                     sha256 != null ? new DigestOutputStream(
    +                                             sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out;
                          InputStream in = con.getInputStream();
                          CountingInputStream cin = new CountingInputStream(in)) {
                         while ((len = cin.read(buf)) >= 0) {
    @@ -1159,6 +1201,14 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
                         byte[] digest = sha1.digest();
                         job.computedSHA1 = Base64.encodeBase64String(digest);
                     }
    +                if (sha256 != null) {
    +                    byte[] digest = sha256.digest();
    +                    job.computedSHA256 = Base64.encodeBase64String(digest);
    +                }
    +                if (sha512 != null) {
    +                    byte[] digest = sha512.digest();
    +                    job.computedSHA512 = Base64.encodeBase64String(digest);
    +                }
                     return tmp;
                 } catch (IOException e) {
                     // assist troubleshooting in case of e.g. "too many redirects" by printing actual URL
    @@ -1169,7 +1219,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
                         // Also, since it involved name resolution, it'd be an expensive operation.
                         extraMessage = " (redirected to: " + con.getURL() + ")";
                     }
    -                throw new IOException2("Failed to download from "+src+extraMessage,e);
    +                throw new IOException("Failed to download from "+src+extraMessage,e);
                 }
             }
     
    @@ -1212,8 +1262,8 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
              *
              * @deprecated as of 1.333
              *      With the introduction of multiple update center capability, this information
    -         *      is now a part of the <tt>update-center.json</tt> file. See
    -         *      <tt>http://jenkins-ci.org/update-center.json</tt> as an example.
    +         *      is now a part of the {@code update-center.json} file. See
    +         *      {@code http://jenkins-ci.org/update-center.json} as an example.
              */
             @Deprecated
             public String getConnectionCheckUrl() {
    @@ -1239,7 +1289,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
              * Returns the URL of the server that hosts plugins and core updates.
              *
              * @deprecated as of 1.333
    -         *      <tt>update-center.json</tt> is now signed, so we don't have to further make sure that
    +         *      {@code update-center.json} is now signed, so we don't have to further make sure that
              *      we aren't downloading from anywhere unsecure.
              */
             @Deprecated
    @@ -1273,7 +1323,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
         /**
          * Things that {@link UpdateCenter#installerService} executes.
          *
    -     * This object will have the <tt>row.jelly</tt> which renders the job on UI.
    +     * This object will have the {@code row.jelly} which renders the job on UI.
          */
         @ExportedBean
         public abstract class UpdateCenterJob implements Runnable {
    @@ -1395,7 +1445,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
                 status = new Running();
                 try {
                     // safeRestart records the current authentication for the log, so set it to the managing user
    -                try (ACLContext _ = ACL.as(User.get(authentication, false, Collections.emptyMap()))) {
    +                try (ACLContext acl = ACL.as(User.get(authentication, false, Collections.emptyMap()))) {
                         Jenkins.getInstance().safeRestart();
                     }
                 } catch (RestartNotSupportedException exception) {
    @@ -1589,11 +1639,18 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
                 status = new Success();
             }
         }
    +
    +    @Restricted(NoExternalUse.class)
    +    /*package*/ interface WithComputedChecksums {
    +        String getComputedSHA1();
    +        String getComputedSHA256();
    +        String getComputedSHA512();
    +    }
         
         /**
          * Base class for a job that downloads a file from the Jenkins project.
          */
    -    public abstract class DownloadJob extends UpdateCenterJob {
    +    public abstract class DownloadJob extends UpdateCenterJob implements WithComputedChecksums {
             /**
              * Immutable object representing the current state of this job.
              */
    @@ -1620,16 +1677,41 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
     
             /**
              * During download, an attempt is made to compute the SHA-1 checksum of the file.
    +         * This is the base64 encoded SHA-1 checksum.
              *
              * @since 1.641
              */
             @CheckForNull
    -        protected String getComputedSHA1() {
    +        public String getComputedSHA1() {
                 return computedSHA1;
             }
     
             private String computedSHA1;
     
    +        /**
    +         * Base64 encoded SHA-256 checksum of the downloaded file, if it could be computed.
    +         *
    +         * @since 2.130
    +         */
    +        @CheckForNull
    +        public String getComputedSHA256() {
    +            return computedSHA256;
    +        }
    +
    +        private String computedSHA256;
    +
    +        /**
    +         * Base64 encoded SHA-512 checksum of the downloaded file, if it could be computed.
    +         *
    +         * @since 2.130
    +         */
    +        @CheckForNull
    +        public String getComputedSHA512() {
    +            return computedSHA512;
    +        }
    +
    +        private String computedSHA512;
    +
             private Authentication authentication;
     
             /**
    @@ -1794,22 +1876,88 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
         }
     
         /**
    -     * If expectedSHA1 is non-null, ensure that actualSha1 is the same value, otherwise throw.
    -     *
    -     * Utility method for InstallationJob and HudsonUpgradeJob.
    +     * Compare the provided values and return the appropriate {@link VerificationResult}.
          *
    -     * @throws IOException when checksums don't match, or actual checksum was null.
          */
    -    private void verifyChecksums(String expectedSHA1, String actualSha1, File downloadedFile) throws IOException {
    -        if (expectedSHA1 != null) {
    -            if (actualSha1 == null) {
    -                // refuse to install if SHA-1 could not be computed
    +    private static VerificationResult verifyChecksums(String expectedDigest, String actualDigest, boolean caseSensitive) {
    +        if (expectedDigest == null) {
    +            return VerificationResult.NOT_PROVIDED;
    +        }
    +
    +        if (actualDigest == null) {
    +            return VerificationResult.NOT_COMPUTED;
    +        }
    +
    +        if (caseSensitive ? expectedDigest.equals(actualDigest) : expectedDigest.equalsIgnoreCase(actualDigest)) {
    +            return VerificationResult.PASS;
    +        }
    +
    +        return VerificationResult.FAIL;
    +    }
    +
    +    private static enum VerificationResult {
    +        PASS,
    +        NOT_PROVIDED,
    +        NOT_COMPUTED,
    +        FAIL
    +    }
    +
    +    /**
    +     * Throws an {@code IOException} with a message about {@code actual} not matching {@code expected} for {@code file} when using {@code algorithm}.
    +     */
    +    private static void throwVerificationFailure(String expected, String actual, File file, String algorithm) throws IOException {
    +        throw new IOException("Downloaded file " + file.getAbsolutePath() + " does not match expected " + algorithm + ", expected '" + expected + "', actual '" + actual + "'");
    +    }
    +
    +    /**
    +     * Implements the checksum verification logic with fallback to weaker algorithm for {@link DownloadJob}.
    +     * @param job The job downloading the file to check
    +     * @param entry The metadata entry for the file to check
    +     * @param file The downloaded file
    +     * @throws IOException thrown when one of the checks failed, or no checksum could be computed.
    +     */
    +    @VisibleForTesting
    +    @Restricted(NoExternalUse.class)
    +    /* package */ static void verifyChecksums(WithComputedChecksums job, UpdateSite.Entry entry, File file) throws IOException {
    +        VerificationResult result512 = verifyChecksums(entry.getSha512(), job.getComputedSHA512(), false);
    +        switch (result512) {
    +            case PASS:
    +                // this has passed so no reason to check the weaker checksums
    +                return;
    +            case FAIL:
    +                throwVerificationFailure(entry.getSha512(), job.getComputedSHA512(), file, "SHA-512");
    +            case NOT_COMPUTED:
    +                LOGGER.log(WARNING, "Attempt to verify a downloaded file (" + file.getName() + ") using SHA-512 failed since it could not be computed. Falling back to weaker algorithms. Update your JRE.");
    +                break;
    +            case NOT_PROVIDED:
    +                break;
    +        }
    +
    +        VerificationResult result256 = verifyChecksums(entry.getSha256(), job.getComputedSHA256(), false);
    +        switch (result256) {
    +            case PASS:
    +                return;
    +            case FAIL:
    +                throwVerificationFailure(entry.getSha256(), job.getComputedSHA256(), file, "SHA-256");
    +            case NOT_COMPUTED:
    +            case NOT_PROVIDED:
    +                break;
    +        }
    +
    +        if (result512 == VerificationResult.NOT_PROVIDED && result256 == VerificationResult.NOT_PROVIDED) {
    +            LOGGER.log(INFO, "Attempt to verify a downloaded file (" + file.getName() + ") using SHA-512 or SHA-256 failed since your configured update site does not provide either of those checksums. Falling back to SHA-1.");
    +        }
    +
    +        VerificationResult result1 = verifyChecksums(entry.getSha1(), job.getComputedSHA1(), true);
    +        switch (result1) {
    +            case PASS:
    +                return;
    +            case FAIL:
    +                throwVerificationFailure(entry.getSha1(), job.getComputedSHA1(), file, "SHA-1");
    +            case NOT_COMPUTED:
                     throw new IOException("Failed to compute SHA-1 of downloaded file, refusing installation");
    -            }
    -            if (!expectedSHA1.equals(actualSha1)) {
    -                throw new IOException("Downloaded file " + downloadedFile.getAbsolutePath() + " does not match expected SHA-1, expected '" + expectedSHA1 + "', actual '" + actualSha1 + "'");
    -                // keep 'downloadedFile' around for investigating what's going on
    -            }
    +            case NOT_PROVIDED:
    +                throw new IOException("Unable to confirm integrity of downloaded file, refusing installation");
             }
         }
     
    @@ -1959,8 +2107,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
              */
             @Override
             protected void replace(File dst, File src) throws IOException {
    -
    -            verifyChecksums(plugin.getSha1(), getComputedSHA1(), src);
    +            if (!site.getId().equals(ID_UPLOAD)) {
    +                verifyChecksums(this, plugin, src);
    +            }
     
                 File bak = Util.changeExtension(dst, ".bak");
                 bak.delete();
    @@ -2094,8 +2243,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
     
             @Override
             protected void replace(File dst, File src) throws IOException {
    -            String expectedSHA1 = site.getData().core.getSha1();
    -            verifyChecksums(expectedSHA1, getComputedSHA1(), src);
    +            verifyChecksums(this, site.getData().core, src);
                 Lifecycle.get().rewriteHudsonWar(src);
             }
         }
    @@ -2159,8 +2307,37 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
             public int compareTo(PluginEntry o) {
                 int r = category.compareTo(o.category);
                 if (r==0) r = plugin.name.compareToIgnoreCase(o.plugin.name);
    +            if (r==0) r = new VersionNumber(plugin.version).compareTo(new VersionNumber(o.plugin.version));
                 return r;
             }
    +
    +        @Override
    +        public boolean equals(Object o) {
    +            if (this == o) {
    +                return true;
    +            }
    +            if (o == null || getClass() != o.getClass()) {
    +                return false;
    +            }
    +
    +            PluginEntry that = (PluginEntry) o;
    +
    +            if (!category.equals(that.category)) {
    +                return false;
    +            }
    +            if (!plugin.name.equals(that.plugin.name)) {
    +                return false;
    +            }
    +            return plugin.version.equals(that.plugin.version);
    +        }
    +
    +        @Override
    +        public int hashCode() {
    +            int result = category.hashCode();
    +            result = 31 * result + plugin.name.hashCode();
    +            result = 31 * result + plugin.version.hashCode();
    +            return result;
    +        }
         }
     
         /**
    @@ -2198,13 +2375,27 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
             }
         }
     
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(UpdateCenter.class.getName() + ".skipPermissionCheck");
    +
    +
         /**
          * Sequence number generator.
          */
         private static final AtomicInteger iota = new AtomicInteger();
     
    -    private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName());
    -
         /**
          * @deprecated as of 1.333
          *      Use {@link UpdateSite#neverUpdate}
    diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java
    index ddf399ceca561bf518104071cd8a349c6b4bf115..739229faa85c5d76bfc2ae1eb3463c91315f8498 100644
    --- a/core/src/main/java/hudson/model/UpdateSite.java
    +++ b/core/src/main/java/hudson/model/UpdateSite.java
    @@ -35,8 +35,9 @@ import hudson.model.UpdateCenter.UpdateCenterJob;
     import hudson.util.FormValidation;
     import hudson.util.FormValidation.Kind;
     import hudson.util.HttpResponses;
    +import static jenkins.util.MemoryReductionUtil.*;
     import hudson.util.TextFile;
    -import static hudson.util.TimeUnit2.*;
    +import static java.util.concurrent.TimeUnit.*;
     import hudson.util.VersionNumber;
     import java.io.File;
     import java.io.IOException;
    @@ -46,7 +47,6 @@ import java.net.URLEncoder;
     import java.security.GeneralSecurityException;
     import java.util.ArrayList;
     import java.util.Collections;
    -import java.util.HashMap;
     import java.util.HashSet;
     import java.util.List;
     import java.util.Locale;
    @@ -56,6 +56,7 @@ import java.util.TreeMap;
     import java.util.UUID;
     import java.util.concurrent.Callable;
     import java.util.concurrent.Future;
    +import java.util.function.Predicate;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import java.util.regex.Pattern;
    @@ -69,6 +70,7 @@ import jenkins.model.DownloadSettings;
     import jenkins.security.UpdateSiteWarningsConfiguration;
     import jenkins.util.JSONSignatureValidator;
     import jenkins.util.SystemProperties;
    +import jenkins.util.java.JavaUtils;
     import net.sf.json.JSONArray;
     import net.sf.json.JSONException;
     import net.sf.json.JSONObject;
    @@ -120,11 +122,6 @@ public class UpdateSite {
          */
         private transient volatile long retryWindow;
     
    -    /**
    -     * lastModified time of the data file when it was last read.
    -     */
    -    private transient long dataLastReadFromFile;
    -
         /**
          * Latest data as read from the data file.
          */
    @@ -136,7 +133,7 @@ public class UpdateSite {
         private final String id;
     
         /**
    -     * Path to <tt>update-center.json</tt>, like <tt>http://jenkins-ci.org/update-center.json</tt>.
    +     * Path to {@code update-center.json}, like {@code http://jenkins-ci.org/update-center.json}.
          */
         private final String url;
     
    @@ -226,6 +223,7 @@ public class UpdateSite {
             LOGGER.info("Obtained the latest update center data file for UpdateSource " + id);
             retryWindow = 0;
             getDataFile().write(json);
    +        data = new Data(o);
             return FormValidation.ok();
         }
     
    @@ -309,23 +307,20 @@ public class UpdateSite {
         public HttpResponse doInvalidateData() {
             Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             dataTimestamp = 0;
    +        data = null;
             return HttpResponses.ok();
         }
     
         /**
    -     * Loads the update center data, if any and if modified since last read.
    +     * Loads the update center data, if any.
          *
          * @return  null if no data is available.
          */
         public Data getData() {
    -        TextFile df = getDataFile();
    -        if (df.exists() && dataLastReadFromFile != df.file.lastModified()) {
    +        if (data == null) {
                 JSONObject o = getJSONObject();
    -            if (o!=null) {
    +            if (o != null) {
                     data = new Data(o);
    -                dataLastReadFromFile = df.file.lastModified();
    -            } else {
    -                data = null;
                 }
             }
             return data;
    @@ -485,18 +480,6 @@ public class UpdateSite {
          */
         @Deprecated
         public String getDownloadUrl() {
    -        /*
    -            HACKISH:
    -
    -            Loading scripts in HTTP from HTTPS pages cause browsers to issue a warning dialog.
    -            The elegant way to solve the problem is to always load update center from HTTPS,
    -            but our backend mirroring scheme isn't ready for that. So this hack serves regular
    -            traffic in HTTP server, and only use HTTPS update center for Jenkins in HTTPS.
    -
    -            We'll monitor the traffic to see if we can sustain this added traffic.
    -         */
    -        if (url.equals("http://updates.jenkins-ci.org/update-center.json") && Jenkins.getInstance().isRootUrlSecure())
    -            return "https"+url.substring(4);
             return url;
         }
     
    @@ -504,7 +487,15 @@ public class UpdateSite {
          * Is this the legacy default update center site?
          */
         public boolean isLegacyDefault() {
    -        return id.equals(UpdateCenter.PREDEFINED_UPDATE_SITE_ID) && url.startsWith("http://hudson-ci.org/") || url.startsWith("http://updates.hudson-labs.org/");
    +        return isHudsonCI() || isUpdatesFromHudsonLabs();
    +    }
    +
    +    private boolean isHudsonCI() {
    +        return url != null && UpdateCenter.PREDEFINED_UPDATE_SITE_ID.equals(id) && url.startsWith("http://hudson-ci.org/");
    +    }
    +
    +    private boolean isUpdatesFromHudsonLabs() {
    +        return url != null && url.startsWith("http://updates.hudson-labs.org/");
         }
     
         /**
    @@ -538,7 +529,7 @@ public class UpdateSite {
             public final String connectionCheckUrl;
     
             Data(JSONObject o) {
    -            this.sourceId = (String)o.get("id");
    +            this.sourceId = Util.intern((String)o.get("id"));
                 JSONObject c = o.optJSONObject("core");
                 if (c!=null) {
                     core = new Entry(sourceId, c, url);
    @@ -568,7 +559,7 @@ public class UpdateSite {
                             }
                         }
                     }
    -                plugins.put(e.getKey(), p);
    +                plugins.put(Util.intern(e.getKey()), p);
                 }
     
                 connectionCheckUrl = (String)o.get("connectionCheckUrl");
    @@ -628,18 +619,26 @@ public class UpdateSite {
             @Restricted(NoExternalUse.class)
             /* final */ String sha1;
     
    +        @Restricted(NoExternalUse.class)
    +        /* final */ String sha256;
    +
    +        @Restricted(NoExternalUse.class)
    +        /* final */ String sha512;
    +
             public Entry(String sourceId, JSONObject o) {
                 this(sourceId, o, null);
             }
     
             Entry(String sourceId, JSONObject o, String baseURL) {
                 this.sourceId = sourceId;
    -            this.name = o.getString("name");
    -            this.version = o.getString("version");
    +            this.name = Util.intern(o.getString("name"));
    +            this.version = Util.intern(o.getString("version"));
     
                 // Trim this to prevent issues when the other end used Base64.encodeBase64String that added newlines
                 // to the end in old commons-codec. Not the case on updates.jenkins-ci.org, but let's be safe.
                 this.sha1 = Util.fixEmptyAndTrim(o.optString("sha1"));
    +            this.sha256 = Util.fixEmptyAndTrim(o.optString("sha256"));
    +            this.sha512 = Util.fixEmptyAndTrim(o.optString("sha512"));
     
                 String url = o.getString("url");
                 if (!URI.create(url).isAbsolute()) {
    @@ -661,6 +660,24 @@ public class UpdateSite {
                 return sha1;
             }
     
    +        /**
    +         * The base64 encoded SHA-256 checksum of the file.
    +         * Can be null if not provided by the update site.
    +         * @since 2.130
    +         */
    +        public String getSha256() {
    +            return sha256;
    +        }
    +
    +        /**
    +         * The base64 encoded SHA-512 checksum of the file.
    +         * Can be null if not provided by the update site.
    +         * @since 2.130
    +         */
    +        public String getSha512() {
    +            return sha512;
    +        }
    +
             /**
              * Checks if the specified "current version" is older than the version of this entry.
              *
    @@ -723,8 +740,8 @@ public class UpdateSite {
     
             public WarningVersionRange(JSONObject o) {
                 this.name = Util.fixEmpty(o.optString("name"));
    -            this.firstVersion = Util.fixEmpty(o.optString("firstVersion"));
    -            this.lastVersion = Util.fixEmpty(o.optString("lastVersion"));
    +            this.firstVersion = Util.intern(Util.fixEmpty(o.optString("firstVersion")));
    +            this.lastVersion = Util.intern(Util.fixEmpty(o.optString("lastVersion")));
                 Pattern p;
                 try {
                     p = Pattern.compile(o.getString("pattern"));
    @@ -821,13 +838,13 @@ public class UpdateSite {
                     this.type = Type.UNKNOWN;
                 }
                 this.id = o.getString("id");
    -            this.component = o.getString("name");
    +            this.component = Util.intern(o.getString("name"));
                 this.message = o.getString("message");
                 this.url = o.getString("url");
     
                 if (o.has("versions")) {
    -                List<WarningVersionRange> ranges = new ArrayList<>();
                     JSONArray versions = o.getJSONArray("versions");
    +                List<WarningVersionRange> ranges = new ArrayList<>(versions.size());
                     for (int i = 0; i < versions.size(); i++) {
                         WarningVersionRange range = new WarningVersionRange(versions.getJSONObject(i));
                         ranges.add(range);
    @@ -911,6 +928,16 @@ public class UpdateSite {
             }
         }
     
    +    private static String get(JSONObject o, String prop) {
    +        if(o.has(prop))
    +            return o.getString(prop);
    +        else
    +            return null;
    +    }
    +
    +    static final Predicate<Object> IS_DEP_PREDICATE = x -> x instanceof JSONObject && get(((JSONObject)x), "name") != null;
    +    static final Predicate<Object> IS_NOT_OPTIONAL = x-> "false".equals(get(((JSONObject)x), "optional"));
    +
         public final class Plugin extends Entry {
             /**
              * Optional URL to the Wiki page that discusses this plugin.
    @@ -941,6 +968,13 @@ public class UpdateSite {
              */
             @Exported
             public final String requiredCore;
    +        /**
    +         * Version of Java this plugin requires to run.
    +         *
    +         * @since TODO
    +         */
    +        @Exported
    +        public final String minimumJavaVersion;
             /**
              * Categories for grouping plugins, taken from labels assigned to wiki page.
              * Can be null.
    @@ -952,13 +986,13 @@ public class UpdateSite {
              * Dependencies of this plugin, a name -&gt; version mapping.
              */
             @Exported
    -        public final Map<String,String> dependencies = new HashMap<String,String>();
    +        public final Map<String,String> dependencies;
             
             /**
              * Optional dependencies of this plugin.
              */
             @Exported
    -        public final Map<String,String> optionalDependencies = new HashMap<String,String>();
    +        public final Map<String,String> optionalDependencies;
     
             @DataBoundConstructor
             public Plugin(String sourceId, JSONObject o) {
    @@ -966,30 +1000,32 @@ public class UpdateSite {
                 this.wiki = get(o,"wiki");
                 this.title = get(o,"title");
                 this.excerpt = get(o,"excerpt");
    -            this.compatibleSinceVersion = get(o,"compatibleSinceVersion");
    -            this.requiredCore = get(o,"requiredCore");
    -            this.categories = o.has("labels") ? (String[])o.getJSONArray("labels").toArray(new String[0]) : null;
    +            this.compatibleSinceVersion = Util.intern(get(o,"compatibleSinceVersion"));
    +            this.minimumJavaVersion = Util.intern(get(o, "minimumJavaVersion"));
    +            this.requiredCore = Util.intern(get(o,"requiredCore"));
    +            this.categories = o.has("labels") ? internInPlace((String[])o.getJSONArray("labels").toArray(EMPTY_STRING_ARRAY)) : null;
    +            JSONArray ja = o.getJSONArray("dependencies");
    +            int depCount = (int)(ja.stream().filter(IS_DEP_PREDICATE.and(IS_NOT_OPTIONAL)).count());
    +            int optionalDepCount = (int)(ja.stream().filter(IS_DEP_PREDICATE.and(IS_NOT_OPTIONAL.negate())).count());
    +            dependencies = getPresizedMutableMap(depCount);
    +            optionalDependencies = getPresizedMutableMap(optionalDepCount);
    +
                 for(Object jo : o.getJSONArray("dependencies")) {
                     JSONObject depObj = (JSONObject) jo;
                     // Make sure there's a name attribute and that the optional value isn't true.
    -                if (get(depObj,"name")!=null) {
    +                String depName = Util.intern(get(depObj,"name"));
    +                if (depName!=null) {
                         if (get(depObj, "optional").equals("false")) {
    -                        dependencies.put(get(depObj, "name"), get(depObj, "version"));
    +                        dependencies.put(depName, Util.intern(get(depObj, "version")));
                         } else {
    -                        optionalDependencies.put(get(depObj, "name"), get(depObj, "version"));
    +                        optionalDependencies.put(depName, Util.intern(get(depObj, "version")));
                         }
                     }
    -                
                 }
     
             }
     
    -        private String get(JSONObject o, String prop) {
    -            if(o.has(prop))
    -                return o.getString(prop);
    -            else
    -                return null;
    -        }
    +
     
             public String getDisplayName() {
                 String displayName;
    @@ -1039,13 +1075,13 @@ public class UpdateSite {
                 List<Plugin> deps = new ArrayList<Plugin>();
     
                 for(Map.Entry<String,String> e : dependencies.entrySet()) {
    -                Plugin depPlugin = Jenkins.getInstance().getUpdateCenter().getPlugin(e.getKey());
    +                VersionNumber requiredVersion = e.getValue() != null ? new VersionNumber(e.getValue()) : null;
    +                Plugin depPlugin = Jenkins.getInstance().getUpdateCenter().getPlugin(e.getKey(), requiredVersion);
                     if (depPlugin == null) {
                         LOGGER.log(Level.WARNING, "Could not find dependency {0} of {1}", new Object[] {e.getKey(), name});
                         continue;
                     }
    -                VersionNumber requiredVersion = new VersionNumber(e.getValue());
    -                
    +
                     // Is the plugin installed already? If not, add it.
                     PluginWrapper current = depPlugin.getInstalled();
     
    @@ -1064,11 +1100,11 @@ public class UpdateSite {
                 }
     
                 for(Map.Entry<String,String> e : optionalDependencies.entrySet()) {
    -                Plugin depPlugin = Jenkins.getInstance().getUpdateCenter().getPlugin(e.getKey());
    +                VersionNumber requiredVersion = e.getValue() != null ? new VersionNumber(e.getValue()) : null;
    +                Plugin depPlugin = Jenkins.getInstance().getUpdateCenter().getPlugin(e.getKey(), requiredVersion);
                     if (depPlugin == null) {
                         continue;
                     }
    -                VersionNumber requiredVersion = new VersionNumber(e.getValue());
     
                     PluginWrapper current = depPlugin.getInstalled();
     
    @@ -1091,6 +1127,21 @@ public class UpdateSite {
                 }
             }
     
    +        /**
    +         * Returns true iff the plugin declares a minimum Java version and it's newer than what the Jenkins master is running on.
    +         * @since TODO
    +         */
    +        public boolean isForNewerJava() {
    +            try {
    +                final VersionNumber currentRuntimeJavaVersion = JavaUtils.getCurrentJavaRuntimeVersionNumber();
    +                return minimumJavaVersion != null && new VersionNumber(minimumJavaVersion).isNewerThan(
    +                        currentRuntimeJavaVersion);
    +            } catch (NumberFormatException nfe) {
    +                logBadMinJavaVersion();
    +                return false; // treat this as undeclared minimum Java version
    +            }
    +        }
    +
             public VersionNumber getNeededDependenciesRequiredCore() {
                 VersionNumber versionNumber = null;
                 try {
    @@ -1105,9 +1156,62 @@ public class UpdateSite {
                 return versionNumber;
             }
     
    +        /**
    +         * Returns the minimum Java version needed to use the plugin and all its dependencies.
    +         * @since TODO
    +         * @return the minimum Java version needed to use the plugin and all its dependencies, or null if unspecified.
    +         */
    +        @CheckForNull
    +        public VersionNumber getNeededDependenciesMinimumJavaVersion() {
    +            VersionNumber versionNumber = null;
    +            try {
    +                versionNumber = minimumJavaVersion == null ? null : new VersionNumber(minimumJavaVersion);
    +            } catch (NumberFormatException nfe) {
    +                logBadMinJavaVersion();
    +            }
    +            for (Plugin p: getNeededDependencies()) {
    +                VersionNumber v = p.getNeededDependenciesMinimumJavaVersion();
    +                if (v == null) {
    +                    continue;
    +                }
    +                if (versionNumber == null || v.isNewerThan(versionNumber)) {
    +                    versionNumber = v;
    +                }
    +            }
    +            return versionNumber;
    +        }
    +
    +        private void logBadMinJavaVersion() {
    +            LOGGER.log(Level.WARNING, "minimumJavaVersion was specified for plugin {0} but unparseable (received {1})",
    +                       new String[]{this.name, this.minimumJavaVersion});
    +        }
    +
             public boolean isNeededDependenciesForNewerJenkins() {
    +            return isNeededDependenciesForNewerJenkins(new PluginManager.MetadataCache());
    +        }
    +
    +        @Restricted(NoExternalUse.class) // table.jelly
    +        public boolean isNeededDependenciesForNewerJenkins(PluginManager.MetadataCache cache) {
    +            return cache.of("isNeededDependenciesForNewerJenkins:" + name, Boolean.class, () -> {
    +                for (Plugin p : getNeededDependencies()) {
    +                    if (p.isForNewerHudson() || p.isNeededDependenciesForNewerJenkins()) {
    +                        return true;
    +                    }
    +                }
    +                return false;
    +            });
    +        }
    +
    +        /**
    +         * Returns true iff any of the plugin dependencies require a newer Java than Jenkins is running on.
    +         *
    +         * @since TODO
    +         */
    +        public boolean isNeededDependenciesForNewerJava() {
                 for (Plugin p: getNeededDependencies()) {
    -                if (p.isForNewerHudson() || p.isNeededDependenciesForNewerJenkins()) return true;
    +                if (p.isForNewerJava() || p.isNeededDependenciesForNewerJava()) {
    +                    return true;
    +                }
                 }
                 return false;
             }
    @@ -1121,11 +1225,19 @@ public class UpdateSite {
              * specified, it'll return true.
              */
             public boolean isNeededDependenciesCompatibleWithInstalledVersion() {
    -            for (Plugin p: getNeededDependencies()) {
    -                if (!p.isCompatibleWithInstalledVersion() || !p.isNeededDependenciesCompatibleWithInstalledVersion())
    -                    return false;
    -            }
    -            return true;
    +            return isNeededDependenciesCompatibleWithInstalledVersion(new PluginManager.MetadataCache());
    +        }
    +
    +        @Restricted(NoExternalUse.class) // table.jelly
    +        public boolean isNeededDependenciesCompatibleWithInstalledVersion(PluginManager.MetadataCache cache) {
    +            return cache.of("isNeededDependenciesCompatibleWithInstalledVersion:" + name, Boolean.class, () -> {
    +                for (Plugin p : getNeededDependencies()) {
    +                    if (!p.isCompatibleWithInstalledVersion() || !p.isNeededDependenciesCompatibleWithInstalledVersion()) {
    +                        return false;
    +                    }
    +                }
    +                return true;
    +            });
             }
     
             /**
    @@ -1134,15 +1246,9 @@ public class UpdateSite {
             @CheckForNull
             @Restricted(NoExternalUse.class)
             public Set<Warning> getWarnings() {
    -            ExtensionList<UpdateSiteWarningsConfiguration> list = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class);
    -            if (list.size() == 0) {
    -                return Collections.emptySet();
    -            }
    -
    +            UpdateSiteWarningsConfiguration configuration = ExtensionList.lookupSingleton(UpdateSiteWarningsConfiguration.class);
                 Set<Warning> warnings = new HashSet<>();
     
    -            UpdateSiteWarningsConfiguration configuration = list.get(0);
    -
                 for (Warning warning: configuration.getAllWarnings()) {
                     if (configuration.isIgnored(warning)) {
                         // warning is currently being ignored
    diff --git a/core/src/main/java/hudson/model/UsageStatistics.java b/core/src/main/java/hudson/model/UsageStatistics.java
    index 22d400891518cfc2c7944d550fe7a1a096947b6f..bb791d38e15d82c220d80ca22eba02f8fc5f5c52 100644
    --- a/core/src/main/java/hudson/model/UsageStatistics.java
    +++ b/core/src/main/java/hudson/model/UsageStatistics.java
    @@ -29,7 +29,7 @@ import hudson.Util;
     import hudson.Extension;
     import hudson.node_monitors.ArchitectureMonitor.DescriptorImpl;
     import hudson.util.Secret;
    -import static hudson.util.TimeUnit2.DAYS;
    +import static java.util.concurrent.TimeUnit.DAYS;
     
     import jenkins.model.Jenkins;
     import net.sf.json.JSONObject;
    @@ -66,7 +66,7 @@ import jenkins.util.SystemProperties;
      * @author Kohsuke Kawaguchi
      */
     @Extension
    -public class UsageStatistics extends PageDecorator {
    +public class UsageStatistics extends PageDecorator implements PersistentDescriptor {
         private final String keyImage;
     
         /**
    @@ -88,7 +88,6 @@ public class UsageStatistics extends PageDecorator {
          */
         public UsageStatistics(String keyImage) {
             this.keyImage = keyImage;
    -        load();
         }
     
         /**
    diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java
    index 0ebd2342e91d7b0a6b5f514720824b0705e48b80..8355aa7f4bef46392c8c2da790cfe4515d3b9cd6 100644
    --- a/core/src/main/java/hudson/model/User.java
    +++ b/core/src/main/java/hudson/model/User.java
    @@ -1,19 +1,19 @@
     /*
      * The MIT License
    - * 
    - * Copyright (c) 2004-2012, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,
    - * Tom Huybrechts, Vincent Latombe
    - * 
    + *
    + * Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,
    + * Tom Huybrechts, Vincent Latombe, CloudBees, Inc.
    + *
      * Permission is hereby granted, free of charge, to any person obtaining a copy
      * of this software and associated documentation files (the "Software"), to deal
      * in the Software without restriction, including without limitation the rights
      * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      * copies of the Software, and to permit persons to whom the Software is
      * furnished to do so, subject to the following conditions:
    - * 
    + *
      * The above copyright notice and this permission notice shall be included in
      * all copies or substantial portions of the Software.
    - * 
    + *
      * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    @@ -34,27 +34,26 @@ import hudson.ExtensionPoint;
     import hudson.FeedAdapter;
     import hudson.Util;
     import hudson.XmlFile;
    +import hudson.init.InitMilestone;
    +import hudson.init.Initializer;
     import hudson.model.Descriptor.FormException;
     import hudson.model.listeners.SaveableListener;
     import hudson.security.ACL;
     import hudson.security.AccessControlled;
    -import hudson.security.Permission;
     import hudson.security.SecurityRealm;
     import hudson.security.UserMayOrMayNotExistException;
     import hudson.util.FormApply;
     import hudson.util.FormValidation;
     import hudson.util.RunList;
     import hudson.util.XStream2;
    +
     import java.io.File;
    -import java.io.FileFilter;
     import java.io.IOException;
     import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collection;
     import java.util.Collections;
    -import java.util.Comparator;
     import java.util.HashSet;
    -import java.util.Iterator;
     import java.util.List;
     import java.util.Map;
     import java.util.Objects;
    @@ -62,16 +61,14 @@ import java.util.Set;
     import java.util.concurrent.ConcurrentHashMap;
     import java.util.concurrent.ConcurrentMap;
     import java.util.concurrent.ExecutionException;
    -import java.util.concurrent.locks.ReadWriteLock;
    -import java.util.concurrent.locks.ReentrantReadWriteLock;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import javax.annotation.Nullable;
    -import javax.annotation.concurrent.GuardedBy;
     import javax.servlet.ServletException;
     import javax.servlet.http.HttpServletResponse;
    +
     import jenkins.model.IdStrategy;
     import jenkins.model.Jenkins;
     import jenkins.model.ModelObjectWithContextMenu;
    @@ -86,11 +83,13 @@ import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
     import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
     import org.acegisecurity.userdetails.UserDetails;
     import org.acegisecurity.userdetails.UsernameNotFoundException;
    -import org.apache.commons.io.filefilter.DirectoryFileFilter;
     import org.apache.commons.lang.StringUtils;
     import org.jenkinsci.Symbol;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
    +import org.kohsuke.stapler.HttpResponses;
    +import org.kohsuke.stapler.Stapler;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.export.Exported;
    @@ -98,6 +97,8 @@ import org.kohsuke.stapler.export.ExportedBean;
     import org.kohsuke.stapler.interceptor.RequirePOST;
     import org.springframework.dao.DataAccessException;
     
    +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
    +
     /**
      * Represents a user.
      *
    @@ -119,11 +120,46 @@ import org.springframework.dao.DataAccessException;
      * is explicitly invoked (perhaps as a result of a browser submitting a
      * configuration.)
      *
    - *
      * @author Kohsuke Kawaguchi
      */
     @ExportedBean
    -public class User extends AbstractModelObject implements AccessControlled, DescriptorByNameOwner, Saveable, Comparable<User>, ModelObjectWithContextMenu {
    +public class User extends AbstractModelObject implements AccessControlled, DescriptorByNameOwner, Saveable, Comparable<User>, ModelObjectWithContextMenu, StaplerProxy {
    +
    +    public static final XStream2 XSTREAM = new XStream2();
    +    private static final Logger LOGGER = Logger.getLogger(User.class.getName());
    +    static final String CONFIG_XML = "config.xml";
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(User.class.getName() + ".skipPermissionCheck");
    +
    +    /**
    +     * Jenkins now refuses to let the user login if he/she doesn't exist in {@link SecurityRealm},
    +     * which was necessary to make sure users removed from the backend will get removed from the frontend.
    +     * <p>
    +     * Unfortunately this infringed some legitimate use cases of creating Jenkins-local users for
    +     * automation purposes. This escape hatch switch can be enabled to resurrect that behaviour.
    +     * <p>
    +     * @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-22346">JENKINS-22346</a>.
    +     */
    +    public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName() + ".allowNonExistentUserToLogin");
    +
    +    /**
    +     * Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission
    +     * accesses a /user/arbitraryName URL.
    +     * <p>
    +     * Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers
    +     * of ephemeral user records, so the behavior was changed in Jenkins 2.TODO / 2.32.2.
    +     * <p>
    +     * As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This
    +     * is not recommended.
    +     * <p>
    +     * SECURITY-406.
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + ".allowUserCreationViaUrl");
     
         /**
          * The username of the 'unknown' user used to avoid null user references.
    @@ -136,23 +172,72 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          */
         private static final String[] ILLEGAL_PERSISTED_USERNAMES = new String[]{ACL.ANONYMOUS_USERNAME,
                 ACL.SYSTEM_USERNAME, UNKNOWN_USERNAME};
    -    private transient final String id;
     
    +    private final int version = 10; // Not currently used, but it may be helpful in the future to store a version.
    +    private String id;
         private volatile String fullName;
    -
         private volatile String description;
     
    -    /**
    -     * List of {@link UserProperty}s configured for this project.
    -     */
         @CopyOnWrite
    -    private volatile List<UserProperty> properties = new ArrayList<UserProperty>();
    +    private volatile List<UserProperty> properties = new ArrayList<>();
     
    +    static {
    +        XSTREAM.alias("user", User.class);
    +    }
     
         private User(String id, String fullName) {
             this.id = id;
             this.fullName = fullName;
    -        load();
    +        load(id);
    +    }
    +
    +    private void load(String userId) {
    +        clearExistingProperties();
    +        loadFromUserConfigFile(userId);
    +        removeNullsThatFailedToLoad();
    +        allocateDefaultPropertyInstancesAsNeeded();
    +        setUserToProperties();
    +    }
    +
    +    private void setUserToProperties() {
    +        for (UserProperty p : properties) {
    +            p.setUser(this);
    +        }
    +    }
    +
    +    private void allocateDefaultPropertyInstancesAsNeeded() {
    +        for (UserPropertyDescriptor d : UserProperty.all()) {
    +            if (getProperty(d.clazz) == null) {
    +                UserProperty up = d.newInstance(this);
    +                if (up != null)
    +                    properties.add(up);
    +            }
    +        }
    +    }
    +
    +    private void removeNullsThatFailedToLoad() {
    +        properties.removeIf(Objects::isNull);
    +    }
    +
    +    private void loadFromUserConfigFile(String userId) {
    +        XmlFile config = getConfigFile();
    +        try {
    +            if ( config != null && config.exists()) {
    +                config.unmarshal(this);
    +                this.id = userId;
    +            }
    +        } catch (IOException e) {
    +            LOGGER.log(Level.SEVERE, "Failed to load " + config, e);
    +        }
    +    }
    +
    +    private void clearExistingProperties() {
    +        properties.clear();
    +    }
    +
    +    private XmlFile getConfigFile() {
    +        File existingUserFolder = getExistingUserFolder();
    +        return existingUserFolder == null ? null : new XmlFile(XSTREAM, new File(existingUserFolder, CONFIG_XML));
         }
     
         /**
    @@ -164,7 +249,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          */
         @Nonnull
         public static IdStrategy idStrategy() {
    -        Jenkins j = Jenkins.getInstance();
    +        Jenkins j = Jenkins.get();
             SecurityRealm realm = j.getSecurityRealm();
             if (realm == null) {
                 return IdStrategy.CASE_INSENSITIVE;
    @@ -172,71 +257,41 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             return realm.getUserIdStrategy();
         }
     
    -    public int compareTo(User that) {
    +    public int compareTo(@Nonnull User that) {
             return idStrategy().compare(this.id, that.id);
         }
     
    -    /**
    -     * Loads the other data from disk if it's available.
    -     */
    -    private synchronized void load() {
    -        properties.clear();
    -
    -        XmlFile config = getConfigFile();
    -        try {
    -            if(config.exists())
    -                config.unmarshal(this);
    -        } catch (IOException e) {
    -            LOGGER.log(Level.SEVERE, "Failed to load "+config,e);
    -        }
    -
    -        // remove nulls that have failed to load
    -        for (Iterator<UserProperty> itr = properties.iterator(); itr.hasNext();) {
    -            if(itr.next()==null)
    -                itr.remove();            
    -        }
    -
    -        // allocate default instances if needed.
    -        // doing so after load makes sure that newly added user properties do get reflected
    -        for (UserPropertyDescriptor d : UserProperty.all()) {
    -            if(getProperty(d.clazz)==null) {
    -                UserProperty up = d.newInstance(this);
    -                if(up!=null)
    -                    properties.add(up);
    -            }
    -        }
    -
    -        for (UserProperty p : properties)
    -            p.setUser(this);
    -    }
    -
         @Exported
         public String getId() {
             return id;
         }
     
    -    public @Nonnull String getUrl() {
    -        return "user/"+Util.rawEncode(idStrategy().keyFor(id));
    +    public @Nonnull
    +    String getUrl() {
    +        return "user/" + Util.rawEncode(idStrategy().keyFor(id));
         }
     
    -    public @Nonnull String getSearchUrl() {
    -        return "/user/"+Util.rawEncode(idStrategy().keyFor(id));
    +    public @Nonnull
    +    String getSearchUrl() {
    +        return "/user/" + Util.rawEncode(idStrategy().keyFor(id));
         }
     
         /**
          * The URL of the user page.
          */
    -    @Exported(visibility=999)
    -    public @Nonnull String getAbsoluteUrl() {
    -        return Jenkins.getInstance().getRootUrl()+getUrl();
    +    @Exported(visibility = 999)
    +    public @Nonnull
    +    String getAbsoluteUrl() {
    +        return Jenkins.get().getRootUrl() + getUrl();
         }
     
         /**
          * Gets the human readable name of this user.
          * This is configurable by the user.
          */
    -    @Exported(visibility=999)
    -    public @Nonnull String getFullName() {
    +    @Exported(visibility = 999)
    +    public @Nonnull
    +    String getFullName() {
             return fullName;
         }
     
    @@ -245,18 +300,19 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          * If the input parameter is empty, the user's ID will be set.
          */
         public void setFullName(String name) {
    -        if(Util.fixEmptyAndTrim(name)==null)    name=id;
    +        if (Util.fixEmptyAndTrim(name) == null) name = id;
             this.fullName = name;
         }
     
         @Exported
    -    public @CheckForNull String getDescription() {
    +    public @CheckForNull
    +    String getDescription() {
             return description;
         }
     
    -
         /**
          * Sets the description of the user.
    +     *
          * @since 1.609
          */
         public void setDescription(String description) {
    @@ -266,7 +322,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         /**
          * Gets the user properties configured for this user.
          */
    -    public Map<Descriptor<UserProperty>,UserProperty> getProperties() {
    +    public Map<Descriptor<UserProperty>, UserProperty> getProperties() {
             return Descriptor.toMap(properties);
         }
     
    @@ -275,8 +331,8 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          */
         public synchronized void addProperty(@Nonnull UserProperty p) throws IOException {
             UserProperty old = getProperty(p.getClass());
    -        List<UserProperty> ps = new ArrayList<UserProperty>(properties);
    -        if(old!=null)
    +        List<UserProperty> ps = new ArrayList<>(properties);
    +        if (old != null)
                 ps.remove(old);
             ps.add(p);
             p.setUser(this);
    @@ -287,17 +343,21 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         /**
          * List of all {@link UserProperty}s exposed primarily for the remoting API.
          */
    -    @Exported(name="property",inline=true)
    +    @Exported(name = "property", inline = true)
         public List<UserProperty> getAllProperties() {
    -        return Collections.unmodifiableList(properties);
    +        if (hasPermission(Jenkins.ADMINISTER)) {
    +            return Collections.unmodifiableList(properties);
    +        }
    +
    +        return Collections.emptyList();
         }
    -    
    +
         /**
          * Gets the specific property, or null.
          */
         public <T extends UserProperty> T getProperty(Class<T> clazz) {
             for (UserProperty p : properties) {
    -            if(clazz.isInstance(p))
    +            if (clazz.isInstance(p))
                     return clazz.cast(p);
             }
             return null;
    @@ -305,46 +365,91 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
     
         /**
          * Creates an {@link Authentication} object that represents this user.
    -     *
    +     * <p>
          * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm.
          * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will
          * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has
          * logged in.
          *
    -     * @throws UsernameNotFoundException
    -     *      If this user is not a valid user in the backend {@link SecurityRealm}.
    +     * @throws UsernameNotFoundException If this user is not a valid user in the backend {@link SecurityRealm}.
          * @since 1.419
          */
    -    public @Nonnull Authentication impersonate() throws UsernameNotFoundException {
    +    public @Nonnull
    +    Authentication impersonate() throws UsernameNotFoundException {
    +        return this.impersonate(this.getUserDetailsForImpersonation());
    +    }
    +
    +    /**
    +     * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm.
    +     * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will
    +     * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has
    +     * logged in.
    +     *
    +     * @return userDetails for the user, in case he's not found but seems legitimate, we provide a userDetails with minimum access
    +     * @throws UsernameNotFoundException If this user is not a valid user in the backend {@link SecurityRealm}.
    +     */
    +    public @Nonnull
    +    UserDetails getUserDetailsForImpersonation() throws UsernameNotFoundException {
    +        ImpersonatingUserDetailsService userDetailsService = new ImpersonatingUserDetailsService(
    +                Jenkins.get().getSecurityRealm().getSecurityComponents().userDetails
    +        );
    +
             try {
    -            UserDetails u = new ImpersonatingUserDetailsService(
    -                    Jenkins.getInstance().getSecurityRealm().getSecurityComponents().userDetails).loadUserByUsername(id);
    -            return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities());
    +            UserDetails userDetails = userDetailsService.loadUserByUsername(id);
    +            LOGGER.log(Level.FINE, "Impersonation of the user {0} was a success", id);
    +            return userDetails;
             } catch (UserMayOrMayNotExistException e) {
    -            // backend can't load information about other users. so use the stored information if available
    +            LOGGER.log(Level.FINE, "The user {0} may or may not exist in the SecurityRealm, so we provide minimum access", id);
             } catch (UsernameNotFoundException e) {
    -            // if the user no longer exists in the backend, we need to refuse impersonating this user
    -            if (!ALLOW_NON_EXISTENT_USER_TO_LOGIN)
    +            if (ALLOW_NON_EXISTENT_USER_TO_LOGIN) {
    +                LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm but we are required to let it pass, due to ALLOW_NON_EXISTENT_USER_TO_LOGIN", id);
    +            } else {
    +                LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm", id);
                     throw e;
    +            }
             } catch (DataAccessException e) {
                 // seems like it's in the same boat as UserMayOrMayNotExistException
    +            LOGGER.log(Level.FINE, "The user {0} retrieval just threw a DataAccess exception with msg = {1}, so we provide minimum access", new Object[]{id, e.getMessage()});
             }
     
    -        // seems like a legitimate user we have no idea about. proceed with minimum access
    -        return new UsernamePasswordAuthenticationToken(id, "",
    -            new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY});
    +        return new LegitimateButUnknownUserDetails(id);
    +    }
    +
    +    /**
    +     * Only used for a legitimate user we have no idea about. We give it only minimum access
    +     */
    +    private static class LegitimateButUnknownUserDetails extends org.acegisecurity.userdetails.User {
    +        private LegitimateButUnknownUserDetails(String username) throws IllegalArgumentException {
    +            super(
    +                    username, "",
    +                    true, true, true, true,
    +                    new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}
    +            );
    +        }
    +    }
    +
    +    /**
    +     * Creates an {@link Authentication} object that represents this user using the given userDetails
    +     *
    +     * @param userDetails Provided by {@link #getUserDetailsForImpersonation()}.
    +     * @see #getUserDetailsForImpersonation()
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public @Nonnull
    +    Authentication impersonate(@Nonnull UserDetails userDetails) {
    +        return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), "", userDetails.getAuthorities());
         }
     
         /**
          * Accepts the new description.
          */
         @RequirePOST
    -    public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
    +    public void doSubmitDescription(StaplerRequest req, StaplerResponse rsp) throws IOException {
             checkPermission(Jenkins.ADMINISTER);
     
             description = req.getParameter("description");
             save();
    -        
    +
             rsp.sendRedirect(".");  // go to the top page
         }
     
    @@ -360,126 +465,68 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         /**
          * Gets the {@link User} object by its id or full name.
          *
    -     * @param create
    -     *      If true, this method will never return null for valid input
    -     *      (by creating a new {@link User} object if none exists.)
    -     *      If false, this method will return null if {@link User} object
    -     *      with the given name doesn't exist.
    +     * @param create If true, this method will never return null for valid input
    +     *               (by creating a new {@link User} object if none exists.)
    +     *               If false, this method will return null if {@link User} object
    +     *               with the given name doesn't exist.
          * @return Requested user. May be {@code null} if a user does not exist and
    -     *      {@code create} is false.
    +     * {@code create} is false.
          * @deprecated use {@link User#get(String, boolean, java.util.Map)}
          */
         @Deprecated
    -    public static @Nullable User get(String idOrFullName, boolean create) {
    +    public static @Nullable
    +    User get(String idOrFullName, boolean create) {
             return get(idOrFullName, create, Collections.emptyMap());
         }
     
         /**
          * Gets the {@link User} object by its id or full name.
    +     * <p>
    +     * In order to resolve the user ID, the method invokes {@link CanonicalIdResolver} extension points.
    +     * Note that it may cause significant performance degradation.
    +     * If you are sure the passed value is a User ID, it is recommended to use {@link #getById(String, boolean)}.
          *
    -     * @param create
    -     *      If true, this method will never return null for valid input
    -     *      (by creating a new {@link User} object if none exists.)
    -     *      If false, this method will return null if {@link User} object
    -     *      with the given name doesn't exist.
    -     *
    -     * @param context
    -     *      contextual environment this user idOfFullName was retrieved from,
    -     *      that can help resolve the user ID
    -     * 
    -     * @return
    -     *      An existing or created user. May be {@code null} if a user does not exist and
    -     *      {@code create} is false.
    +     * @param create  If true, this method will never return null for valid input
    +     *                (by creating a new {@link User} object if none exists.)
    +     *                If false, this method will return null if {@link User} object
    +     *                with the given name doesn't exist.
    +     * @param context contextual environment this user idOfFullName was retrieved from,
    +     *                that can help resolve the user ID
    +     * @return An existing or created user. May be {@code null} if a user does not exist and
    +     * {@code create} is false.
          */
    -    public static @Nullable User get(String idOrFullName, boolean create, Map context) {
    -
    -        if(idOrFullName==null)
    +    public static @Nullable
    +    User get(String idOrFullName, boolean create, @Nonnull Map context) {
    +        if (idOrFullName == null) {
                 return null;
    -
    -        // sort resolvers by priority
    -        List<CanonicalIdResolver> resolvers = new ArrayList<CanonicalIdResolver>(ExtensionList.lookup(CanonicalIdResolver.class));
    -        Collections.sort(resolvers);
    -
    -        String id = null;
    -        for (CanonicalIdResolver resolver : resolvers) {
    -            id = resolver.resolveCanonicalId(idOrFullName, context);
    -            if (id != null) {
    -                LOGGER.log(Level.FINE, "{0} mapped {1} to {2}", new Object[] {resolver, idOrFullName, id});
    -                break;
    -            }
             }
    -        // DefaultUserCanonicalIdResolver will always return a non-null id if all other CanonicalIdResolver failed
    -        if (id == null) {
    -            throw new IllegalStateException("The user id should be always non-null thanks to DefaultUserCanonicalIdResolver");
    +
    +        User user = AllUsers.get(idOrFullName);
    +        if (user != null) {
    +            return user;
             }
    -        return getOrCreate(id, idOrFullName, create);
    +
    +        String id = CanonicalIdResolver.resolve(idOrFullName, context);
    +        return getOrCreateById(id, idOrFullName, create);
         }
     
         /**
          * Retrieve a user by its ID, and create a new one if requested.
    -     * @return
    -     *      An existing or created user. May be {@code null} if a user does not exist and
    -     *      {@code create} is false.
    +     *
    +     * @return An existing or created user. May be {@code null} if a user does not exist and
    +     * {@code create} is false.
          */
    -    private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create) {
    -        String idkey = idStrategy().keyFor(id);
    -
    -        byNameLock.readLock().lock();
    -        User u;
    -        try {
    -            u = byName.get(idkey);
    -        } finally {
    -            byNameLock.readLock().unlock();
    -        }
    -        final File configFile = getConfigFileFor(id);
    -        if (u == null && !configFile.isFile() && !configFile.getParentFile().isDirectory()) {
    -            // check for legacy users and migrate if safe to do so.
    -            File[] legacy = getLegacyConfigFilesFor(id);
    -            if (legacy != null && legacy.length > 0) {
    -                for (File legacyUserDir : legacy) {
    -                    final XmlFile legacyXml = new XmlFile(XSTREAM, new File(legacyUserDir, "config.xml"));
    -                    try {
    -                        Object o = legacyXml.read();
    -                        if (o instanceof User) {
    -                            if (idStrategy().equals(id, legacyUserDir.getName()) && !idStrategy().filenameOf(legacyUserDir.getName())
    -                                    .equals(legacyUserDir.getName())) {
    -                                if (!legacyUserDir.renameTo(configFile.getParentFile())) {
    -                                    LOGGER.log(Level.WARNING, "Failed to migrate user record from {0} to {1}",
    -                                            new Object[]{legacyUserDir, configFile.getParentFile()});
    -                                }
    -                                break;
    -                            }
    -                        } else {
    -                            LOGGER.log(Level.FINE, "Unexpected object loaded from {0}: {1}",
    -                                    new Object[]{ legacyUserDir, o });
    -                        }
    -                    } catch (IOException e) {
    -                        LOGGER.log(Level.FINE, String.format("Exception trying to load user from %s: %s",
    -                                new Object[]{ legacyUserDir, e.getMessage() }), e);
    -                    }
    -                }
    -            }
    -        }
    -        if (u==null && (create || configFile.exists())) {
    -            User tmp = new User(id, fullName);
    -            User prev;
    -            byNameLock.readLock().lock();
    -            try {
    -                prev = byName.putIfAbsent(idkey, u = tmp);
    -            } finally {
    -                byNameLock.readLock().unlock();
    -            }
    -            if (prev != null) {
    -                u = prev; // if some has already put a value in the map, use it
    -                if (LOGGER.isLoggable(Level.FINE) && !fullName.equals(prev.getFullName())) {
    -                    LOGGER.log(Level.FINE, "mismatch on fullName (‘" + fullName + "’ vs. ‘" + prev.getFullName() + "’) for ‘" + id + "’", new Throwable());
    -                }
    -            } else if (!id.equals(fullName) && !configFile.exists()) {
    -                // JENKINS-16332: since the fullName may not be recoverable from the id, and various code may store the id only, we must save the fullName
    +    private static @Nullable
    +    User getOrCreateById(@Nonnull String id, @Nonnull String fullName, boolean create) {
    +        User u = AllUsers.get(id);
    +        if (u == null && (create || UserIdMapper.getInstance().isMapped(id))) {
    +            u = new User(id, fullName);
    +            AllUsers.put(id, u);
    +            if (!id.equals(fullName) && !UserIdMapper.getInstance().isMapped(id)) {
                     try {
                         u.save();
                     } catch (IOException x) {
    -                    LOGGER.log(Level.WARNING, null, x);
    +                    LOGGER.log(Level.WARNING, "Failed to save user configuration for " + id, x);
                     }
                 }
             }
    @@ -488,153 +535,141 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
     
         /**
          * Gets the {@link User} object by its id or full name.
    +     * <p>
    +     * Creates a user on-demand.
    +     *
    +     * <p>
    +     * Use {@link #getById} when you know you have an ID.
    +     * In this method Jenkins will try to resolve the {@link User} by full name with help of various
    +     * {@link hudson.tasks.UserNameResolver}.
    +     * This is slow (see JENKINS-23281).
    +     *
    +     * @deprecated This method is deprecated, because it causes unexpected {@link User} creation
    +     * by API usage code and causes performance degradation of used to retrieve users by ID.
          * Use {@link #getById} when you know you have an ID.
    +     * Otherwise use {@link #getOrCreateByIdOrFullName(String)} or {@link #get(String, boolean, Map)}.
          */
    -    public static @Nonnull User get(String idOrFullName) {
    -        return get(idOrFullName,true);
    +    @Deprecated
    +    public static @Nonnull
    +    User get(String idOrFullName) {
    +        return getOrCreateByIdOrFullName(idOrFullName);
         }
     
    +    /**
    +     * Get the user by ID or Full Name.
    +     * <p>
    +     * If the user does not exist, creates a new one on-demand.
    +     *
    +     * <p>
    +     * Use {@link #getById} when you know you have an ID.
    +     * In this method Jenkins will try to resolve the {@link User} by full name with help of various
    +     * {@link hudson.tasks.UserNameResolver}.
    +     * This is slow (see JENKINS-23281).
    +     *
    +     * @param idOrFullName User ID or full name
    +     * @return User instance. It will be created on-demand.
    +     * @since 2.91
    +     */
    +    public static @Nonnull User getOrCreateByIdOrFullName(@Nonnull String idOrFullName) {
    +        return get(idOrFullName, true, Collections.emptyMap());
    +    }
    +
    +
         /**
          * Gets the {@link User} object representing the currently logged-in user, or null
          * if the current user is anonymous.
    +     *
          * @since 1.172
          */
    -    public static @CheckForNull User current() {
    +    public static @CheckForNull
    +    User current() {
             return get(Jenkins.getAuthentication());
         }
     
         /**
          * Gets the {@link User} object representing the supplied {@link Authentication} or
          * {@code null} if the supplied {@link Authentication} is either anonymous or {@code null}
    +     *
          * @param a the supplied {@link Authentication} .
          * @return a {@link User} object for the supplied {@link Authentication} or {@code null}
          * @since 1.609
          */
    -    public static @CheckForNull User get(@CheckForNull Authentication a) {
    -        if(a == null || a instanceof AnonymousAuthenticationToken)
    +    public static @CheckForNull
    +    User get(@CheckForNull Authentication a) {
    +        if (a == null || a instanceof AnonymousAuthenticationToken)
                 return null;
     
    -        // Since we already know this is a name, we can just call getOrCreate with the name directly.
    -        String id = a.getName();
    -        return getById(id, true);
    +        // Since we already know this is a name, we can just call getOrCreateById with the name directly.
    +        return getById(a.getName(), true);
         }
     
         /**
          * Gets the {@link User} object by its <code>id</code>
          *
    -     * @param id
    -     *            the id of the user to retrieve and optionally create if it does not exist.
    -     * @param create
    -     *            If <code>true</code>, this method will never return <code>null</code> for valid input (by creating a
    -     *            new {@link User} object if none exists.) If <code>false</code>, this method will return
    -     *            <code>null</code> if {@link User} object with the given id doesn't exist.
    +     * @param id     the id of the user to retrieve and optionally create if it does not exist.
    +     * @param create If <code>true</code>, this method will never return <code>null</code> for valid input (by creating a
    +     *               new {@link User} object if none exists.) If <code>false</code>, this method will return
    +     *               <code>null</code> if {@link User} object with the given id doesn't exist.
          * @return the a User whose id is <code>id</code>, or <code>null</code> if <code>create</code> is <code>false</code>
    -     *         and the user does not exist.
    +     * and the user does not exist.
    +     * @since 1.651.2 / 2.3
          */
    -    public static @Nullable User getById(String id, boolean create) {
    -        return getOrCreate(id, id, create);
    +    public static @Nullable
    +    User getById(String id, boolean create) {
    +        return getOrCreateById(id, id, create);
         }
     
    -    private static volatile long lastScanned;
    -
         /**
          * Gets all the users.
          */
    -    public static @Nonnull Collection<User> getAll() {
    +    public static @Nonnull
    +    Collection<User> getAll() {
             final IdStrategy strategy = idStrategy();
    -        if(System.currentTimeMillis() -lastScanned>10000) {
    -            // occasionally scan the file system to check new users
    -            // whether we should do this only once at start up or not is debatable.
    -            // set this right away to avoid another thread from doing the same thing while we do this.
    -            // having two threads doing the work won't cause race condition, but it's waste of time.
    -            lastScanned = System.currentTimeMillis();
    -
    -            File[] subdirs = getRootDir().listFiles((FileFilter)DirectoryFileFilter.INSTANCE);
    -            if(subdirs==null)       return Collections.emptyList(); // shall never happen
    -
    -            for (File subdir : subdirs)
    -                if(new File(subdir,"config.xml").exists()) {
    -                    String name = strategy.idFromFilename(subdir.getName());
    -                    User.getOrCreate(name, name, true);
    -                }
    -
    -            lastScanned = System.currentTimeMillis();
    -        }
    -
    -        byNameLock.readLock().lock();
    -        ArrayList<User> r;
    -        try {
    -            r = new ArrayList<User>(byName.values());
    -        } finally {
    -            byNameLock.readLock().unlock();
    -        }
    -        Collections.sort(r,new Comparator<User>() {
    -
    -            public int compare(User o1, User o2) {
    -                return strategy.compare(o1.getId(), o2.getId());
    -            }
    -        });
    -        return r;
    -    }
    -
    -    /**
    -     * Reloads the configuration from disk.
    -     */
    -    public static void reload() {
    -        byNameLock.readLock().lock();
    -        try {
    -            for (User u : byName.values()) {
    -                u.load();
    -            }
    -        } finally {
    -            byNameLock.readLock().unlock();
    -            UserDetailsCache.get().invalidateAll();
    -        }
    +        ArrayList<User> users = new ArrayList<>(AllUsers.values());
    +        users.sort((o1, o2) -> strategy.compare(o1.getId(), o2.getId()));
    +        return users;
         }
     
         /**
    -     * Stop gap hack. Don't use it. To be removed in the trunk.
    +     * To be called from {@link Jenkins#reload} only.
          */
    -    public static void clear() {
    -        byNameLock.writeLock().lock();
    -        try {
    -            byName.clear();
    -        } finally {
    -            byNameLock.writeLock().unlock();
    -        }
    +    @Restricted(NoExternalUse.class)
    +    public static void reload() throws IOException {
    +        UserIdMapper.getInstance().reload();
    +        AllUsers.reload();
         }
     
         /**
          * Called when changing the {@link IdStrategy}.
    +     *
          * @since 1.566
          */
         public static void rekey() {
    -        final IdStrategy strategy = idStrategy();
    -        byNameLock.writeLock().lock();
    +        /* There are many and varied ways in which this could cause erratic or
    +            problematic behavior. Such changes should really only occur during initial
    +            setup and under very controlled situations. After this sort of a change
    +            the whole webapp should restart. It's possible that this rekeying,
    +            or greater issues in the realm change, could affect currently logged
    +            in users and even the user making the change. */
             try {
    -            for (Map.Entry<String, User> e : byName.entrySet()) {
    -                String idkey = strategy.keyFor(e.getValue().id);
    -                if (!idkey.equals(e.getKey())) {
    -                    // need to remap
    -                    byName.remove(e.getKey());
    -                    byName.putIfAbsent(idkey, e.getValue());
    -                }
    -            }
    -        } finally {
    -            byNameLock.writeLock().unlock();
    -            UserDetailsCache.get().invalidateAll();
    +            reload();
    +        } catch (IOException e) {
    +            LOGGER.log(Level.SEVERE, "Failed to perform rekey operation.", e);
             }
         }
     
         /**
          * Returns the user name.
          */
    -    public @Nonnull String getDisplayName() {
    +    public @Nonnull
    +    String getDisplayName() {
             return getFullName();
         }
     
    -    /** true if {@link AbstractBuild#hasParticipant} or {@link hudson.model.Cause.UserIdCause} */
    -    private boolean relatedTo(@Nonnull AbstractBuild<?,?> b) {
    +    /**
    +     * true if {@link AbstractBuild#hasParticipant} or {@link hudson.model.Cause.UserIdCause}
    +     */
    +    private boolean relatedTo(@Nonnull AbstractBuild<?, ?> b) {
             if (b.hasParticipant(this)) {
                 return true;
             }
    @@ -655,56 +690,73 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          */
         @SuppressWarnings("unchecked")
         @WithBridgeMethods(List.class)
    -    public @Nonnull RunList getBuilds() {
    -        return RunList.fromJobs((Iterable)Jenkins.getInstance().allItems(Job.class)).filter(new Predicate<Run<?,?>>() {
    -            @Override public boolean apply(Run<?,?> r) {
    -                return r instanceof AbstractBuild && relatedTo((AbstractBuild<?,?>) r);
    -            }
    -        });
    +    public @Nonnull
    +    RunList getBuilds() {
    +        return RunList.fromJobs((Iterable) Jenkins.get().
    +                allItems(Job.class)).filter((Predicate<Run<?, ?>>) r -> r instanceof AbstractBuild && relatedTo((AbstractBuild<?, ?>) r));
         }
     
         /**
          * Gets all the {@link AbstractProject}s that this user has committed to.
    +     *
          * @since 1.191
          */
    -    public @Nonnull Set<AbstractProject<?,?>> getProjects() {
    -        Set<AbstractProject<?,?>> r = new HashSet<AbstractProject<?,?>>();
    -        for (AbstractProject<?,?> p : Jenkins.getInstance().allItems(AbstractProject.class))
    -            if(p.hasParticipant(this))
    +    public @Nonnull
    +    Set<AbstractProject<?, ?>> getProjects() {
    +        Set<AbstractProject<?, ?>> r = new HashSet<>();
    +        for (AbstractProject<?, ?> p : Jenkins.get().allItems(AbstractProject.class))
    +            if (p.hasParticipant(this))
                     r.add(p);
             return r;
         }
     
    -    public @Override String toString() {
    +    public @Override
    +    String toString() {
             return fullName;
         }
     
         /**
    -     * The file we save our configuration.
    +     * Called by tests in the JTH. Otherwise this shouldn't be called.
    +     * Even in the tests this usage is questionable.
          */
    -    protected final XmlFile getConfigFile() {
    -        return new XmlFile(XSTREAM,getConfigFileFor(id));
    +    @Deprecated
    +    public static void clear() {
    +        if (ExtensionList.lookup(AllUsers.class).isEmpty()) {
    +            return;
    +        }
    +        UserIdMapper.getInstance().clear();
    +        AllUsers.clear();
         }
     
         private static final File getConfigFileFor(String id) {
    -        return new File(getRootDir(), idStrategy().filenameOf(id) +"/config.xml");
    +        return new File(getUserFolderFor(id), "config.xml");
    +    }
    +    
    +    private static File getUserFolderFor(String id){
    +        return new File(getRootDir(), idStrategy().filenameOf(id));
    +    }
    +    /**
    +     * Returns the folder that store all the user information.
    +     * Useful for plugins to save a user-specific file aside the config.xml.
    +     * Exposes implementation details that may be subject to change.
    +     * 
    +     * @return The folder containing the user configuration files or {@code null} if the user was not yet saved.
    +     *
    +     * @since 2.129
    +     */
    +    public @CheckForNull File getUserFolder() {
    +        return getExistingUserFolder();
         }
     
    -    private static final File[] getLegacyConfigFilesFor(final String id) {
    -        return getRootDir().listFiles(new FileFilter() {
    -            @Override
    -            public boolean accept(File pathname) {
    -                return pathname.isDirectory() && new File(pathname, "config.xml").isFile() && idStrategy().equals(
    -                        pathname.getName(), id);
    -            }
    -        });
    +    private @CheckForNull File getExistingUserFolder() {
    +        return UserIdMapper.getInstance().getDirectory(id);
         }
     
         /**
          * Gets the directory where Hudson stores user information.
          */
    -    private static File getRootDir() {
    -        return new File(Jenkins.getInstance().getRootDir(), "users");
    +    static File getRootDir() {
    +        return new File(Jenkins.get().getRootDir(), "users");
         }
     
         /**
    @@ -717,12 +769,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          *
          * @param id ID to be checked
          * @return {@code true} if the username or fullname is valid.
    -     *      For {@code null} or blank IDs returns {@code false}.
    +     * For {@code null} or blank IDs returns {@code false}.
          * @since 1.600
          */
         public static boolean isIdOrFullnameAllowed(@CheckForNull String id) {
    -        //TODO: StringUtils.isBlank() checks the null value, but FindBugs is not smart enough. Remove it later
    -        if (id == null || StringUtils.isBlank(id)) {
    +        if (StringUtils.isBlank(id)) {
                 return false;
             }
             final String trimmedId = id.trim();
    @@ -734,36 +785,49 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         }
     
         /**
    -     * Save the settings to a file.
    +     * Save the user configuration.
          */
    -    public synchronized void save() throws IOException, FormValidation {
    -        if (! isIdOrFullnameAllowed(id)) {
    +    public synchronized void save() throws IOException {
    +        if (!isIdOrFullnameAllowed(id)) {
                 throw FormValidation.error(Messages.User_IllegalUsername(id));
             }
    -        if (! isIdOrFullnameAllowed(fullName)) {
    +        if (!isIdOrFullnameAllowed(fullName)) {
                 throw FormValidation.error(Messages.User_IllegalFullname(fullName));
             }
    -        if(BulkChange.contains(this))   return;
    -        getConfigFile().write(this);
    -        SaveableListener.fireOnChange(this, getConfigFile());
    +        if (BulkChange.contains(this)) {
    +            return;
    +        }
    +        XmlFile xmlFile = new XmlFile(XSTREAM, constructUserConfigFile());
    +        xmlFile.write(this);
    +        SaveableListener.fireOnChange(this, xmlFile);
    +    }
    +
    +    private File constructUserConfigFile() throws IOException {
    +        return new File(putUserFolderIfAbsent(), CONFIG_XML);
    +    }
    +
    +    private File putUserFolderIfAbsent() throws IOException {
    +        return UserIdMapper.getInstance().putIfAbsent(id, true);
         }
     
         /**
          * Deletes the data directory and removes this user from Hudson.
          *
    -     * @throws IOException
    -     *      if we fail to delete.
    +     * @throws IOException if we fail to delete.
          */
    -    public synchronized void delete() throws IOException {
    -        final IdStrategy strategy = idStrategy();
    -        byNameLock.readLock().lock();
    -        try {
    -            byName.remove(strategy.keyFor(id));
    -        } finally {
    -            byNameLock.readLock().unlock();
    +    public void delete() throws IOException {
    +        String idKey = idStrategy().keyFor(id);
    +        File existingUserFolder = getExistingUserFolder();
    +        UserIdMapper.getInstance().remove(id);
    +        AllUsers.remove(id);
    +        deleteExistingUserFolder(existingUserFolder);
    +        UserDetailsCache.get().invalidate(idKey);
    +    }
    +
    +    private void deleteExistingUserFolder(File existingUserFolder) throws IOException {
    +        if (existingUserFolder != null && existingUserFolder.exists()) {
    +            Util.deleteRecursive(existingUserFolder);
             }
    -        Util.deleteRecursive(new File(getRootDir(), strategy.filenameOf(id)));
    -        UserDetailsCache.get().invalidate(strategy.keyFor(id));
         }
     
         /**
    @@ -777,7 +841,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          * Accepts submission from the configuration page.
          */
         @RequirePOST
    -    public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {
    +    public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
             checkPermission(Jenkins.ADMINISTER);
     
             JSONObject json = req.getSubmittedForm();
    @@ -785,13 +849,13 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             fullName = json.getString("fullName");
             description = json.getString("description");
     
    -        List<UserProperty> props = new ArrayList<UserProperty>();
    +        List<UserProperty> props = new ArrayList<>();
             int i = 0;
             for (UserPropertyDescriptor d : UserProperty.all()) {
                 UserProperty p = getProperty(d.clazz);
     
                 JSONObject o = json.optJSONObject("userProperty" + (i++));
    -            if (o!=null) {
    +            if (o != null) {
                     if (p != null) {
                         p = p.reconfigure(req, o);
                     } else {
    @@ -800,7 +864,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
                     p.setUser(this);
                 }
     
    -            if (p!=null)
    +            if (p != null)
                     props.add(p);
             }
             this.properties = props;
    @@ -811,14 +875,14 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
                 UserDetailsCache.get().invalidate(oldFullName);
             }
     
    -        FormApply.success(".").generateResponse(req,rsp,this);
    +        FormApply.success(".").generateResponse(req, rsp, this);
         }
     
         /**
          * Deletes this user from Hudson.
          */
         @RequirePOST
    -    public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    +    public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException {
             checkPermission(Jenkins.ADMINISTER);
             if (idStrategy().equals(id, Jenkins.getAuthentication().getName())) {
                 rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Cannot delete self");
    @@ -839,9 +903,9 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         }
     
         public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    -        final List<Run> lastBuilds = new ArrayList<Run>();
    -        for (AbstractProject<?,?> p : Jenkins.getInstance().allItems(AbstractProject.class)) {
    -            for (AbstractBuild<?,?> b = p.getLastBuild(); b != null; b = b.getPreviousBuild()) {
    +        final List<Run> lastBuilds = new ArrayList<>();
    +        for (AbstractProject<?, ?> p : Jenkins.get().allItems(AbstractProject.class)) {
    +            for (AbstractBuild<?, ?> b = p.getLastBuild(); b != null; b = b.getPreviousBuild()) {
                     if (relatedTo(b)) {
                         lastBuilds.add(b);
                         break;
    @@ -850,65 +914,22 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             }
             // historically these have been reported sorted by project name, we switched to the lazy iteration
             // so we only have to sort the sublist of runs rather than the full list of irrelevant projects
    -        Collections.sort(lastBuilds, new Comparator<Run>() {
    -            @Override
    -            public int compare(Run o1, Run o2) {
    -                return Items.BY_FULL_NAME.compare(o1.getParent(), o2.getParent());
    -            }
    -        });
    +        lastBuilds.sort((o1, o2) -> Items.BY_FULL_NAME.compare(o1.getParent(), o2.getParent()));
             rss(req, rsp, " latest build", RunList.fromRuns(lastBuilds), Run.FEED_ADAPTER_LATEST);
         }
     
         private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs, FeedAdapter adapter)
                 throws IOException, ServletException {
    -        RSS.forwardToRss(getDisplayName()+ suffix, getUrl(), runs.newBuilds(), adapter, req, rsp);
    -    }
    -
    -    /**
    -     * Keyed by {@link User#id}. This map is used to ensure
    -     * singleton-per-id semantics of {@link User} objects.
    -     *
    -     * The key needs to be generated by {@link IdStrategy#keyFor(String)}.
    -     */
    -    @GuardedBy("byNameLock")
    -    private static final ConcurrentMap<String,User> byName = new ConcurrentHashMap<String, User>();
    -
    -    /**
    -     * This lock is used to guard access to the {@link #byName} map. Use
    -     * {@link java.util.concurrent.locks.ReadWriteLock#readLock()} for normal access and
    -     * {@link java.util.concurrent.locks.ReadWriteLock#writeLock()} for {@link #rekey()} or any other operation
    -     * that requires operating on the map as a whole.
    -     */
    -    private static final ReadWriteLock byNameLock = new ReentrantReadWriteLock();
    -
    -    /**
    -     * Used to load/save user configuration.
    -     */
    -    public static final XStream2 XSTREAM = new XStream2();
    -
    -    private static final Logger LOGGER = Logger.getLogger(User.class.getName());
    -
    -    static {
    -        XSTREAM.alias("user",User.class);
    +        RSS.forwardToRss(getDisplayName() + suffix, getUrl(), runs.newBuilds(), adapter, req, rsp);
         }
     
    +    @Override
    +    @Nonnull
         public ACL getACL() {
    -        final ACL base = Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
    +        ACL base = Jenkins.get().getAuthorizationStrategy().getACL(this);
             // always allow a non-anonymous user full control of himself.
    -        return new ACL() {
    -            public boolean hasPermission(Authentication a, Permission permission) {
    -                return (idStrategy().equals(a.getName(), id) && !(a instanceof AnonymousAuthenticationToken))
    -                        || base.hasPermission(a, permission);
    -            }
    -        };
    -    }
    -
    -    public void checkPermission(Permission permission) {
    -        getACL().checkPermission(permission);
    -    }
    -
    -    public boolean hasPermission(Permission permission) {
    -        return getACL().hasPermission(permission);
    +        return ACL.lambda((a, permission) -> (idStrategy().equals(a.getName(), id) && !(a instanceof AnonymousAuthenticationToken))
    +                || base.hasPermission(a, permission));
         }
     
         /**
    @@ -917,21 +938,23 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         public boolean canDelete() {
             final IdStrategy strategy = idStrategy();
             return hasPermission(Jenkins.ADMINISTER) && !strategy.equals(id, Jenkins.getAuthentication().getName())
    -                && new File(getRootDir(), strategy.filenameOf(id)).exists();
    +                && UserIdMapper.getInstance().isMapped(id);
         }
     
         /**
          * Checks for authorities (groups) associated with this user.
          * If the caller lacks {@link Jenkins#ADMINISTER}, or any problems arise, returns an empty list.
          * {@link SecurityRealm#AUTHENTICATED_AUTHORITY} and the username, if present, are omitted.
    -     * @since 1.498
    +     *
          * @return a possibly empty list
    +     * @since 1.498
          */
    -    public @Nonnull List<String> getAuthorities() {
    -        if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
    +    public @Nonnull
    +    List<String> getAuthorities() {
    +        if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
                 return Collections.emptyList();
             }
    -        List<String> r = new ArrayList<String>();
    +        List<String> r = new ArrayList<>();
             Authentication authentication;
             try {
                 authentication = impersonate();
    @@ -948,33 +971,29 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
                     r.add(n);
                 }
             }
    -        Collections.sort(r, String.CASE_INSENSITIVE_ORDER);
    +        r.sort(String.CASE_INSENSITIVE_ORDER);
             return r;
         }
     
    -    public Descriptor getDescriptorByName(String className) {
    -        return Jenkins.getInstance().getDescriptorByName(className);
    -    }
    -    
         public Object getDynamic(String token) {
    -        for(Action action: getTransientActions()){
    -            if(Objects.equals(action.getUrlName(), token))
    +        for (Action action : getTransientActions()) {
    +            if (Objects.equals(action.getUrlName(), token))
                     return action;
             }
    -        for(Action action: getPropertyActions()){
    -            if(Objects.equals(action.getUrlName(), token))
    +        for (Action action : getPropertyActions()) {
    +            if (Objects.equals(action.getUrlName(), token))
                     return action;
             }
             return null;
         }
    -    
    +
         /**
          * Return all properties that are also actions.
    -     * 
    +     *
          * @return the list can be empty but never null. read only.
          */
         public List<Action> getPropertyActions() {
    -        List<Action> actions = new ArrayList<Action>();
    +        List<Action> actions = new ArrayList<>();
             for (UserProperty userProp : getProperties().values()) {
                 if (userProp instanceof Action) {
                     actions.add((Action) userProp);
    @@ -982,37 +1001,126 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             }
             return Collections.unmodifiableList(actions);
         }
    -    
    +
         /**
          * Return all transient actions associated with this user.
    -     * 
    +     *
          * @return the list can be empty but never null. read only.
          */
         public List<Action> getTransientActions() {
    -        List<Action> actions = new ArrayList<Action>();
    -        for (TransientUserActionFactory factory: TransientUserActionFactory.all()) {
    +        List<Action> actions = new ArrayList<>();
    +        for (TransientUserActionFactory factory : TransientUserActionFactory.all()) {
                 actions.addAll(factory.createFor(this));
             }
             return Collections.unmodifiableList(actions);
         }
     
         public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
    -        return new ContextMenu().from(this,request,response);
    +        return new ContextMenu().from(this, request, response);
         }
    -    
    +
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            if (!Jenkins.get().hasPermission(Jenkins.READ)) {
    +                return null;
    +            }
    +        }
    +        return this;
    +    }
    +
         /**
          * Gets list of Illegal usernames, for which users should not be created.
          * Always includes users from {@link #ILLEGAL_PERSISTED_USERNAMES}
    +     *
          * @return List of usernames
          */
         @Restricted(NoExternalUse.class)
         /*package*/ static Set<String> getIllegalPersistedUsernames() {
    -        // TODO: This method is designed for further extensibility via system properties. To be extended in a follow-up issue
    -        final Set<String> res = new HashSet<>();
    -        res.addAll(Arrays.asList(ILLEGAL_PERSISTED_USERNAMES));
    -        return res;
    +        return new HashSet<>(Arrays.asList(ILLEGAL_PERSISTED_USERNAMES));
    +    }
    +
    +    private Object writeReplace() {
    +        return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this));
    +    }
    +
    +    private static class Replacer {
    +        private final String id;
    +
    +        Replacer(User u) {
    +            id = u.getId();
    +        }
    +
    +        private Object readResolve() {
    +            return getById(id, false);
    +        }
         }
     
    +    /**
    +     * Per-{@link Jenkins} holder of all known {@link User}s.
    +     */
    +    @Extension
    +    @Restricted(NoExternalUse.class)
    +    public static final class AllUsers {
    +
    +        private final ConcurrentMap<String, User> byName = new ConcurrentHashMap<>();
    +
    +        @Initializer(after = InitMilestone.JOB_LOADED)
    +        public static void scanAll() {
    +            for (String userId : UserIdMapper.getInstance().getConvertedUserIds()) {
    +                User user = new User(userId, userId);
    +                getInstance().byName.putIfAbsent(idStrategy().keyFor(userId), user);
    +            }
    +        }
    +
    +        /**
    +         * Keyed by {@link User#id}. This map is used to ensure
    +         * singleton-per-id semantics of {@link User} objects.
    +         * <p>
    +         * The key needs to be generated by {@link IdStrategy#keyFor(String)}.
    +         */
    +        private static AllUsers getInstance() {
    +            return ExtensionList.lookupSingleton(AllUsers.class);
    +        }
    +
    +        private static void reload() {
    +            getInstance().byName.clear();
    +            UserDetailsCache.get().invalidateAll();
    +            scanAll();
    +        }
    +
    +        private static void clear() {
    +            getInstance().byName.clear();
    +        }
    +
    +        private static void remove(String id) {
    +            getInstance().byName.remove(idStrategy().keyFor(id));
    +        }
    +
    +        private static User get(String id) {
    +            return getInstance().byName.get(idStrategy().keyFor(id));
    +        }
    +
    +        private static void put(String id, User user) {
    +            getInstance().byName.putIfAbsent(idStrategy().keyFor(id), user);
    +        }
    +
    +        private static Collection<User> values() {
    +            return getInstance().byName.values();
    +        }
    +    }
    +
    +    /**
    +     * Resolves User IDs by ID, full names or other strings.
    +     * <p>
    +     * This extension point may be useful to map SCM user names to Jenkins {@link User} IDs.
    +     * Currently the extension point is used in {@link User#get(String, boolean, Map)}.
    +     *
    +     * @see jenkins.model.DefaultUserCanonicalIdResolver
    +     * @see FullNameIdResolver
    +     * @since 1.479
    +     */
         public static abstract class CanonicalIdResolver extends AbstractDescribableImpl<CanonicalIdResolver> implements ExtensionPoint, Comparable<CanonicalIdResolver> {
     
             /**
    @@ -1022,30 +1130,76 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
              */
             public static final String REALM = "realm";
     
    -        public int compareTo(CanonicalIdResolver o) {
    +        @Override
    +        public int compareTo(@Nonnull CanonicalIdResolver o) {
                 // reverse priority order
    -            int i = getPriority();
    -            int j = o.getPriority();
    -            return i>j ? -1 : (i==j ? 0:1);
    +            return Integer.compare(o.getPriority(), getPriority());
             }
     
             /**
              * extract user ID from idOrFullName with help from contextual infos.
              * can return <code>null</code> if no user ID matched the input
              */
    -        public abstract @CheckForNull String resolveCanonicalId(String idOrFullName, Map<String, ?> context);
    +        public abstract @CheckForNull
    +        String resolveCanonicalId(String idOrFullName, Map<String, ?> context);
     
    +        /**
    +         * Gets priority of the resolver.
    +         * Higher priority means that it will be checked earlier.
    +         * <p>
    +         * Overriding methods must not use {@link Integer#MIN_VALUE}, because it will cause collisions
    +         * with {@link jenkins.model.DefaultUserCanonicalIdResolver}.
    +         *
    +         * @return Priority of the resolver.
    +         */
             public int getPriority() {
                 return 1;
             }
     
    +        //Such sorting and collection rebuild is not good for User#get(...) method performance.
    +
    +        /**
    +         * Gets all extension points, sorted by priority.
    +         *
    +         * @return Sorted list of extension point implementations.
    +         * @since 2.93
    +         */
    +        public static List<CanonicalIdResolver> all() {
    +            List<CanonicalIdResolver> resolvers = new ArrayList<>(ExtensionList.lookup(CanonicalIdResolver.class));
    +            Collections.sort(resolvers);
    +            return resolvers;
    +        }
    +
    +        /**
    +         * Resolves users using all available {@link CanonicalIdResolver}s.
    +         *
    +         * @param idOrFullName ID or full name of the user
    +         * @param context      Context
    +         * @return Resolved User ID or {@code null} if the user ID cannot be resolved.
    +         * @since 2.93
    +         */
    +        @CheckForNull
    +        public static String resolve(@Nonnull String idOrFullName, @Nonnull Map<String, ?> context) {
    +            for (CanonicalIdResolver resolver : CanonicalIdResolver.all()) {
    +                String id = resolver.resolveCanonicalId(idOrFullName, context);
    +                if (id != null) {
    +                    LOGGER.log(Level.FINE, "{0} mapped {1} to {2}", new Object[]{resolver, idOrFullName, id});
    +                    return id;
    +                }
    +            }
    +
    +            // De-facto it is not going to happen OOTB, because the current DefaultUserCanonicalIdResolver
    +            // always returns a value. But we still need to check nulls if somebody disables the extension point
    +            return null;
    +        }
         }
     
     
         /**
          * Resolve user ID from full name
          */
    -    @Extension @Symbol("fullName")
    +    @Extension
    +    @Symbol("fullName")
         public static class FullNameIdResolver extends CanonicalIdResolver {
     
             @Override
    @@ -1071,15 +1225,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         @Restricted(NoExternalUse.class)
         public static class UserIDCanonicalIdResolver extends User.CanonicalIdResolver {
     
    -        private static /* not final */ boolean SECURITY_243_FULL_DEFENSE = 
    +        private static /* not final */ boolean SECURITY_243_FULL_DEFENSE =
                     SystemProperties.getBoolean(User.class.getName() + ".SECURITY_243_FULL_DEFENSE", true);
     
    -        private static final ThreadLocal<Boolean> resolving = new ThreadLocal<Boolean>() {
    -            @Override
    -            protected Boolean initialValue() {
    -                return false;
    -            }
    -        };
    +        private static final ThreadLocal<Boolean> resolving = ThreadLocal.withInitial(() -> false);
     
             @Override
             public String resolveCanonicalId(String idOrFullName, Map<String, ?> context) {
    @@ -1113,30 +1262,4 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
     
         }
     
    -    /**
    -     * Jenkins now refuses to let the user login if he/she doesn't exist in {@link SecurityRealm},
    -     * which was necessary to make sure users removed from the backend will get removed from the frontend.
    -     * <p>
    -     * Unfortunately this infringed some legitimate use cases of creating Jenkins-local users for
    -     * automation purposes. This escape hatch switch can be enabled to resurrect that behaviour.
    -     *
    -     * JENKINS-22346.
    -     */
    -    public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName()+".allowNonExistentUserToLogin");
    -
    -    /**
    -     * Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission
    -     * accesses a /user/arbitraryName URL.
    -     * <p>
    -     * Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers
    -     * of ephemeral user records, so the behavior was changed in Jenkins 2.TODO / 2.32.2.
    -     * <p>
    -     * As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This
    -     * is not recommended.
    -     *
    -     * SECURITY-406.
    -     */
    -    @Restricted(NoExternalUse.class)
    -    public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + ".allowUserCreationViaUrl");
    -
     }
    diff --git a/core/src/main/java/hudson/model/UserIdMapper.java b/core/src/main/java/hudson/model/UserIdMapper.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..d3cd8c034244dd8bd293abc293a2fbf275c9466c
    --- /dev/null
    +++ b/core/src/main/java/hudson/model/UserIdMapper.java
    @@ -0,0 +1,202 @@
    +/*
    + * The MIT License
    + *
    + * Copyright 2018 CloudBees, Inc.
    + *
    + * Permission is hereby granted, free of charge, to any person obtaining a copy
    + * of this software and associated documentation files (the "Software"), to deal
    + * in the Software without restriction, including without limitation the rights
    + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    + * copies of the Software, and to permit persons to whom the Software is
    + * furnished to do so, subject to the following conditions:
    + *
    + * The above copyright notice and this permission notice shall be included in
    + * all copies or substantial portions of the Software.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    + * THE SOFTWARE.
    + */
    +package hudson.model;
    +
    +import hudson.Extension;
    +import hudson.ExtensionList;
    +import hudson.Util;
    +import hudson.XmlFile;
    +import hudson.init.InitMilestone;
    +import hudson.init.Initializer;
    +import hudson.util.XStream2;
    +import jenkins.model.IdStrategy;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +
    +import javax.annotation.CheckForNull;
    +import java.io.File;
    +import java.io.IOException;
    +import java.nio.file.Files;
    +import java.nio.file.NoSuchFileException;
    +import java.nio.file.Path;
    +import java.util.Collections;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.concurrent.ConcurrentHashMap;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +import java.util.regex.Pattern;
    +
    +@Restricted(NoExternalUse.class)
    +@Extension
    +public class UserIdMapper {
    +
    +    private static final XStream2 XSTREAM = new XStream2();
    +    static final String MAPPING_FILE = "users.xml";
    +    private static final Logger LOGGER = Logger.getLogger(UserIdMapper.class.getName());
    +    private static final int PREFIX_MAX = 15;
    +    private static final Pattern PREFIX_PATTERN = Pattern.compile("[^A-Za-z0-9]");
    +    private final int version = 1; // Not currently used, but it may be helpful in the future to store a version.
    +
    +    private transient File usersDirectory;
    +    private Map<String, String> idToDirectoryNameMap = new ConcurrentHashMap<>();
    +
    +    static UserIdMapper getInstance() {
    +        return ExtensionList.lookupSingleton(UserIdMapper.class);
    +    }
    +
    +    public UserIdMapper() {
    +    }
    +
    +    @Initializer(after = InitMilestone.PLUGINS_STARTED, before = InitMilestone.JOB_LOADED)
    +    public File init() throws IOException {
    +        usersDirectory = createUsersDirectoryAsNeeded();
    +        load();
    +        return usersDirectory;
    +    }
    +
    +    @CheckForNull File getDirectory(String userId) {
    +        String directoryName = idToDirectoryNameMap.get(getIdStrategy().keyFor(userId));
    +        return directoryName == null ? null : new File(usersDirectory, directoryName);
    +    }
    +
    +    File putIfAbsent(String userId, boolean saveToDisk) throws IOException {
    +        String idKey = getIdStrategy().keyFor(userId);
    +        String directoryName = idToDirectoryNameMap.get(idKey);
    +        File directory = null;
    +        if (directoryName == null) {
    +            synchronized (this) {
    +                directoryName = idToDirectoryNameMap.get(idKey);
    +                if (directoryName == null) {
    +                    directory = createDirectoryForNewUser(userId);
    +                    directoryName = directory.getName();
    +                    idToDirectoryNameMap.put(idKey, directoryName);
    +                    if (saveToDisk) {
    +                        save();
    +                    }
    +                }
    +            }
    +        }
    +        return directory == null ? new File(usersDirectory, directoryName) : directory;
    +    }
    +
    +    boolean isMapped(String userId) {
    +        return idToDirectoryNameMap.containsKey(getIdStrategy().keyFor(userId));
    +    }
    +
    +    Set<String> getConvertedUserIds() {
    +        return Collections.unmodifiableSet(idToDirectoryNameMap.keySet());
    +    }
    +
    +    void remove(String userId) throws IOException {
    +        idToDirectoryNameMap.remove(getIdStrategy().keyFor(userId));
    +        save();
    +    }
    +
    +    void clear() {
    +        idToDirectoryNameMap.clear();
    +    }
    +
    +    void reload() throws IOException {
    +        clear();
    +        load();
    +    }
    +
    +    protected IdStrategy getIdStrategy() {
    +        return User.idStrategy();
    +    }
    +
    +    protected File getUsersDirectory() {
    +        return User.getRootDir();
    +    }
    +
    +    private XmlFile getXmlConfigFile() {
    +        File file = getConfigFile(usersDirectory);
    +        return new XmlFile(XSTREAM, file);
    +    }
    +
    +    static File getConfigFile(File usersDirectory) {
    +        return new File(usersDirectory, MAPPING_FILE);
    +    }
    +
    +    private File createDirectoryForNewUser(String userId) throws IOException {
    +        try {
    +            Path tempDirectory = Files.createTempDirectory(Util.fileToPath(usersDirectory), generatePrefix(userId));
    +            return tempDirectory.toFile();
    +        } catch (IOException e) {
    +            LOGGER.log(Level.SEVERE, "Error creating directory for user: " + userId, e);
    +            throw e;
    +        }
    +    }
    +
    +    private String generatePrefix(String userId) {
    +        String fullPrefix = PREFIX_PATTERN.matcher(userId).replaceAll("");
    +        return fullPrefix.length() > PREFIX_MAX - 1 ? fullPrefix.substring(0, PREFIX_MAX - 1) + '_' : fullPrefix + '_';
    +    }
    +
    +    private File createUsersDirectoryAsNeeded() throws IOException {
    +        File usersDirectory = getUsersDirectory();
    +        if (!usersDirectory.exists()) {
    +            try {
    +                Files.createDirectory(usersDirectory.toPath());
    +            } catch (IOException e) {
    +                LOGGER.log(Level.SEVERE, "Unable to create users directory: " + usersDirectory, e);
    +                throw e;
    +            }
    +        }
    +        return usersDirectory;
    +    }
    +
    +    synchronized void save() throws IOException {
    +        try {
    +            getXmlConfigFile().write(this);
    +        } catch (IOException ioe) {
    +            LOGGER.log(Level.WARNING, "Error saving userId mapping file.", ioe);
    +            throw ioe;
    +        }
    +    }
    +
    +    private void load() throws IOException {
    +        UserIdMigrator migrator = new UserIdMigrator(usersDirectory, getIdStrategy());
    +        if (migrator.needsMigration()) {
    +            try {
    +                migrator.migrateUsers(this);
    +            } catch (IOException ioe) {
    +                LOGGER.log(Level.SEVERE, "Error migrating users.", ioe);
    +                throw ioe;
    +            }
    +        } else {
    +            XmlFile config = getXmlConfigFile();
    +            try {
    +                config.unmarshal(this);
    +            } catch (NoSuchFileException e) {
    +                LOGGER.log(Level.FINE, "User id mapping file does not exist. It will be created when a user is saved.");
    +            } catch (IOException e) {
    +                LOGGER.log(Level.WARNING, "Failed to load " + config, e);
    +                throw e;
    +            }
    +        }
    +    }
    +
    +}
    diff --git a/core/src/main/java/hudson/model/UserIdMigrator.java b/core/src/main/java/hudson/model/UserIdMigrator.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..f5b6b2f43f4392fd4b1539ff82cda7d72a54525f
    --- /dev/null
    +++ b/core/src/main/java/hudson/model/UserIdMigrator.java
    @@ -0,0 +1,104 @@
    +/*
    + * The MIT License
    + *
    + * Copyright (c) 2018 CloudBees, Inc.
    + *
    + * Permission is hereby granted, free of charge, to any person obtaining a copy
    + * of this software and associated documentation files (the "Software"), to deal
    + * in the Software without restriction, including without limitation the rights
    + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    + * copies of the Software, and to permit persons to whom the Software is
    + * furnished to do so, subject to the following conditions:
    + *
    + * The above copyright notice and this permission notice shall be included in
    + * all copies or substantial portions of the Software.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    + * THE SOFTWARE.
    + */
    +package hudson.model;
    +
    +import jenkins.model.IdStrategy;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +
    +import java.io.File;
    +import java.io.IOException;
    +import java.nio.file.CopyOption;
    +import java.nio.file.Files;
    +import java.nio.file.StandardCopyOption;
    +import java.util.HashMap;
    +import java.util.Map;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
    +
    +@Restricted(NoExternalUse.class)
    +class UserIdMigrator {
    +
    +    private static final Logger LOGGER = Logger.getLogger(UserIdMigrator.class.getName());
    +    private static final String EMPTY_USERNAME_DIRECTORY_NAME = "emptyUsername";
    +
    +    private final File usersDirectory;
    +    private final IdStrategy idStrategy;
    +
    +    UserIdMigrator(File usersDirectory, IdStrategy idStrategy) {
    +        this.usersDirectory = usersDirectory;
    +        this.idStrategy = idStrategy;
    +    }
    +
    +    boolean needsMigration() {
    +        File mappingFile = UserIdMapper.getConfigFile(usersDirectory);
    +        if (mappingFile.exists() && mappingFile.isFile()) {
    +            LOGGER.finest("User mapping file already exists. No migration needed.");
    +            return false;
    +        }
    +        File[] userDirectories = listUserDirectories();
    +        return userDirectories != null && userDirectories.length > 0;
    +    }
    +
    +    private File[] listUserDirectories() {
    +        return usersDirectory.listFiles(file -> file.isDirectory() && new File(file, User.CONFIG_XML).exists());
    +    }
    +
    +    Map<String, File> scanExistingUsers() throws IOException {
    +        Map<String, File> users = new HashMap<>();
    +        File[] userDirectories = listUserDirectories();
    +        if (userDirectories != null) {
    +            for (File directory : userDirectories) {
    +                String userId = idStrategy.idFromFilename(directory.getName());
    +                users.put(userId, directory);
    +            }
    +        }
    +        addEmptyUsernameIfExists(users);
    +        return users;
    +    }
    +
    +    private void addEmptyUsernameIfExists(Map<String, File> users) throws IOException {
    +        File emptyUsernameConfigFile = new File(usersDirectory, User.CONFIG_XML);
    +        if (emptyUsernameConfigFile.exists()) {
    +            File newEmptyUsernameDirectory = new File(usersDirectory, EMPTY_USERNAME_DIRECTORY_NAME);
    +            Files.createDirectory(newEmptyUsernameDirectory.toPath());
    +            File newEmptyUsernameConfigFile = new File(newEmptyUsernameDirectory, User.CONFIG_XML);
    +            Files.move(emptyUsernameConfigFile.toPath(), newEmptyUsernameConfigFile.toPath());
    +            users.put("", newEmptyUsernameDirectory);
    +        }
    +    }
    +
    +    void migrateUsers(UserIdMapper mapper) throws IOException {
    +        LOGGER.fine("Beginning migration of users to userId mapping.");
    +        Map<String, File> existingUsers = scanExistingUsers();
    +        for (Map.Entry<String, File> existingUser : existingUsers.entrySet()) {
    +            File newDirectory = mapper.putIfAbsent(existingUser.getKey(), false);
    +            LOGGER.log(Level.INFO, "Migrating user '" + existingUser.getKey() + "' from 'users/" + existingUser.getValue().getName() + "/' to 'users/" + newDirectory.getName() + "/'");
    +            Files.move(existingUser.getValue().toPath(), newDirectory.toPath(), StandardCopyOption.REPLACE_EXISTING);
    +        }
    +        mapper.save();
    +        LOGGER.fine("Completed migration of users to userId mapping.");
    +    }
    +
    +}
    diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java
    index 48198a28ebbe9400139f48735b15067cf33bbfcc..ec02b1211b99d78fa15d5024b8ce69f6311fb006 100644
    --- a/core/src/main/java/hudson/model/UserProperty.java
    +++ b/core/src/main/java/hudson/model/UserProperty.java
    @@ -41,10 +41,10 @@ import org.kohsuke.stapler.export.ExportedBean;
      * configuration screen, and they are persisted with the user object.
      *
      * <p>
    - * Configuration screen should be defined in <tt>config.jelly</tt>.
    + * Configuration screen should be defined in {@code config.jelly}.
      * Within this page, the {@link UserProperty} instance is available
    - * as <tt>instance</tt> variable (while <tt>it</tt> refers to {@link User}.
    - * See {@link hudson.search.UserSearchProperty}'s <tt>config.jelly</tt> for an example.
    + * as {@code instance} variable (while {@code it} refers to {@link User}.
    + * See {@link hudson.search.UserSearchProperty}'s {@code config.jelly} for an example.
      * <p>A property may also define a {@code summary.jelly} view to show in the main user screen.
      *
      * @author Kohsuke Kawaguchi
    diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java
    index 375087c2daa63497036704f6bf857135361e8c6e..a2b1dcc24e5ee8133213e700540db9d9aabce484 100644
    --- a/core/src/main/java/hudson/model/View.java
    +++ b/core/src/main/java/hudson/model/View.java
    @@ -26,7 +26,6 @@ package hudson.model;
     
     import com.thoughtworks.xstream.converters.ConversionException;
     import com.thoughtworks.xstream.io.StreamException;
    -import com.thoughtworks.xstream.io.xml.Xpp3Driver;
     import hudson.DescriptorExtensionList;
     import hudson.Extension;
     import hudson.ExtensionPoint;
    @@ -64,6 +63,7 @@ import jenkins.model.item_category.Categories;
     import jenkins.model.item_category.Category;
     import jenkins.model.item_category.ItemCategory;
     import jenkins.scm.RunWithSCM;
    +import jenkins.security.stapler.StaplerAccessibleType;
     import jenkins.util.ProgressiveRendering;
     import jenkins.util.xml.XMLUtils;
     
    @@ -114,9 +114,9 @@ import java.util.Map;
     import java.util.Set;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +import java.util.stream.Collectors;
     
     import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
    -import static jenkins.scm.RunWithSCM.*;
     
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
    @@ -134,7 +134,7 @@ import org.xml.sax.SAXException;
      * <h2>Note for implementers</h2>
      * <ul>
      * <li>
    - * {@link View} subtypes need the <tt>newViewDetail.jelly</tt> page,
    + * {@link View} subtypes need the {@code newViewDetail.jelly} page,
      * which is included in the "new view" page. This page should have some
      * description of what the view is about. 
      * </ul>
    @@ -172,8 +172,6 @@ public abstract class View extends AbstractModelObject implements AccessControll
          */
         protected boolean filterQueue;
         
    -    protected transient List<Action> transientActions;
    -
         /**
          * List of {@link ViewProperty}s configured for this view.
          * @since 1.406
    @@ -192,6 +190,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
         /**
          * Gets all the items in this collection in a read-only view.
          */
    +    @Nonnull
         @Exported(name="jobs")
         public abstract Collection<TopLevelItem> getItems();
     
    @@ -455,25 +454,49 @@ public abstract class View extends AbstractModelObject implements AccessControll
             return false;
         }
     
    +    private final static int FILTER_LOOP_MAX_COUNT = 10;
    +
         private List<Queue.Item> filterQueue(List<Queue.Item> base) {
             if (!isFilterQueue()) {
                 return base;
             }
    -
             Collection<TopLevelItem> items = getItems();
    -        List<Queue.Item> result = new ArrayList<Queue.Item>();
    -        for (Queue.Item qi : base) {
    -            if (items.contains(qi.task)) {
    -                result.add(qi);
    -            } else
    -            if (qi.task instanceof AbstractProject<?, ?>) {
    -                AbstractProject<?,?> project = (AbstractProject<?, ?>) qi.task;
    -                if (items.contains(project.getRootProject())) {
    -                    result.add(qi);
    -                }
    +        return base.stream().filter(qi -> filterQueueItemTest(qi, items))
    +                .collect(Collectors.toList());
    +    }
    +
    +    private boolean filterQueueItemTest(Queue.Item item, Collection<TopLevelItem> viewItems) {
    +        // Check if the task of parent tasks are in the list of viewItems.
    +        // Pipeline jobs and other jobs which allow parts require us to
    +        // check owner tasks as well.
    +        Queue.Task currentTask = item.task;
    +        for (int count = 1;; count++) {
    +            if (viewItems.contains(currentTask)) {
    +                return true;
    +            }
    +            Queue.Task next = currentTask.getOwnerTask();
    +            if (next == currentTask) {
    +                break;
    +            } else {
    +                currentTask = next;
    +            }
    +            if (count == FILTER_LOOP_MAX_COUNT) {
    +                LOGGER.warning(String.format(
    +                        "Failed to find root task for queue item '%s' for " +
    +                        "view '%s' in under %d iterations, aborting!",
    +                        item.getDisplayName(), getDisplayName(),
    +                        FILTER_LOOP_MAX_COUNT));
    +                break;
                 }
             }
    -        return result;
    +        // Check root project for sub-job projects (e.g. matrix jobs).
    +        if (item.task instanceof AbstractProject<?, ?>) {
    +            AbstractProject<?,?> project = (AbstractProject<?, ?>) item.task;
    +            if (viewItems.contains(project.getRootProject())) {
    +                return true;
    +            }
    +        }
    +        return false;
         }
     
         public List<Queue.Item> getQueueItems() {
    @@ -525,21 +548,20 @@ public abstract class View extends AbstractModelObject implements AccessControll
          * @see Jenkins#getActions()
          */
         public List<Action> getActions() {
    -    	List<Action> result = new ArrayList<Action>();
    -    	result.addAll(getOwner().getViewActions());
    -    	synchronized (this) {
    -    		if (transientActions == null) {
    -                updateTransientActions();
    -    		}
    -    		result.addAll(transientActions);
    -    	}
    -    	return result;
    -    }
    -    
    -    public synchronized void updateTransientActions() {
    -        transientActions = TransientViewActionFactory.createAllFor(this); 
    +        List<Action> result = new ArrayList<>();
    +        result.addAll(getOwner().getViewActions());
    +        result.addAll(TransientViewActionFactory.createAllFor(this));
    +        return result;
         }
    -    
    +
    +    /**
    +     * No-op. Included to maintain backwards compatibility.
    +     * @deprecated This method does nothing and should not be used
    +     */
    +    @Restricted(DoNotUse.class)
    +    @Deprecated
    +    public void updateTransientActions() {}
    +
         public Object getDynamic(String token) {
             for (Action a : getActions()) {
                 String url = a.getUrlName();
    @@ -579,14 +601,6 @@ public abstract class View extends AbstractModelObject implements AccessControll
             return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
         }
     
    -    public void checkPermission(Permission p) {
    -        getACL().checkPermission(p);
    -    }
    -
    -    public boolean hasPermission(Permission p) {
    -        return getACL().hasPermission(p);
    -    }
    -
         /** @deprecated Does not work properly with moved jobs. Use {@link ItemListener#onLocationChanged} instead. */
         @Deprecated
         public void onJobRenamed(Item item, String oldName, String newName) {}
    @@ -684,6 +698,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
         }
     
         @ExportedBean
    +    @StaplerAccessibleType
         public static final class People  {
             @Exported
             public final List<UserInfo> users;
    @@ -977,7 +992,6 @@ public abstract class View extends AbstractModelObject implements AccessControll
             rename(req.getParameter("name"));
     
             getProperties().rebuild(req, req.getSubmittedForm(), getApplicablePropertyDescriptors());
    -        updateTransientActions();  
     
             save();
     
    @@ -1054,6 +1068,10 @@ public abstract class View extends AbstractModelObject implements AccessControll
         @Restricted(DoNotUse.class)
         public Categories doItemCategories(StaplerRequest req, StaplerResponse rsp, @QueryParameter String iconStyle) throws IOException, ServletException {
             getOwner().checkPermission(Item.CREATE);
    +
    +        rsp.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    +        rsp.addHeader("Pragma", "no-cache");
    +        rsp.addHeader("Expires", "0");
             Categories categories = new Categories();
             int order = 0;
             JellyContext ctx;
    @@ -1134,7 +1152,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
         }
     
         /**
    -     * Accepts <tt>config.xml</tt> submission, as well as serve it.
    +     * Accepts {@code config.xml} submission, as well as serve it.
          */
         @WebMethod(name = "config.xml")
         public HttpResponse doConfigDotXml(StaplerRequest req) throws IOException {
    @@ -1192,7 +1210,8 @@ public abstract class View extends AbstractModelObject implements AccessControll
                 // Do not allow overwriting view name as it might collide with another
                 // view in same ViewGroup and might not satisfy Jenkins.checkGoodName.
                 String oldname = name;
    -            Object o = Jenkins.XSTREAM.unmarshal(new Xpp3Driver().createReader(in), this);
    +            ViewGroup oldOwner = owner; // oddly, this field is not transient
    +            Object o = Jenkins.XSTREAM2.unmarshal(XStream2.getDefaultDriver().createReader(in), this, null, true);
                 if (!o.getClass().equals(getClass())) {
                     // ensure that we've got the same view type. extending this code to support updating
                     // to different view type requires destroying & creating a new view type
    @@ -1201,6 +1220,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
                         "the view with the new view type.");
                 }
                 name = oldname;
    +            owner = oldOwner;
             } catch (StreamException | ConversionException | Error e) {// mostly reflection errors
                 throw new IOException("Unable to read",e);
             }
    @@ -1345,7 +1365,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
         /**
          * Instantiate View subtype from XML stream.
          *
    -     * @param name Alternative name to use or <tt>null</tt> to keep the one in xml.
    +     * @param name Alternative name to use or {@code null} to keep the one in xml.
          */
         public static View createViewFromXML(String name, InputStream xml) throws IOException {
     
    diff --git a/core/src/main/java/hudson/model/ViewDescriptor.java b/core/src/main/java/hudson/model/ViewDescriptor.java
    index a63484d69e1250aeb4ae2518e47b8e1a2e25b8b7..933ad74dcb68aeccb30cf329781e89ff185ba99e 100644
    --- a/core/src/main/java/hudson/model/ViewDescriptor.java
    +++ b/core/src/main/java/hudson/model/ViewDescriptor.java
    @@ -88,7 +88,6 @@ public abstract class ViewDescriptor extends Descriptor<View> {
          */
         @Restricted(DoNotUse.class)
         public AutoCompletionCandidates doAutoCompleteCopyNewItemFrom(@QueryParameter final String value, @AncestorInPath ItemGroup<?> container) {
    -        // TODO do we need a permissions check here?
             AutoCompletionCandidates candidates = AutoCompletionCandidates.ofJobNames(TopLevelItem.class, value, container);
             if (container instanceof DirectlyModifiableTopLevelItemGroup) {
                 DirectlyModifiableTopLevelItemGroup modifiableContainer = (DirectlyModifiableTopLevelItemGroup) container;
    diff --git a/core/src/main/java/hudson/model/ViewGroupMixIn.java b/core/src/main/java/hudson/model/ViewGroupMixIn.java
    index 359f68f1c534c5af0483015a93fa9b09ca0c4d97..313e07eceb0e8d394d5d9567ede7cb6f0b4db63b 100644
    --- a/core/src/main/java/hudson/model/ViewGroupMixIn.java
    +++ b/core/src/main/java/hudson/model/ViewGroupMixIn.java
    @@ -36,6 +36,7 @@ import java.util.Collection;
     import java.util.Collections;
     import java.util.List;
     import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     
     /**
      * Implements {@link ViewGroup} to be used as a "mix-in".
    @@ -66,27 +67,40 @@ public abstract class ViewGroupMixIn {
         private final ViewGroup owner;
     
         /**
    -     * Returns all the views. This list must be concurrently iterable.
    +     * Returns all views in the group. This list must be modifiable and concurrently iterable.
          */
    +    @Nonnull
         protected abstract List<View> views();
    +
    +    /**
    +     * Gets primary view of the mix-in.
    +     * @return Name of the primary view, {@code null} if there is no primary one defined.
    +     */
    +    @CheckForNull
         protected abstract String primaryView();
    +
    +    /**
    +     * Sets the primary view.
    +     * @param newName Name of the primary view to be set.
    +     *                {@code null} to make the primary view undefined.
    +     */
         protected abstract void primaryView(String newName);
     
         protected ViewGroupMixIn(ViewGroup owner) {
             this.owner = owner;
         }
     
    -    public void addView(View v) throws IOException {
    +    public void addView(@Nonnull View v) throws IOException {
             v.owner = owner;
             views().add(v);
             owner.save();
         }
     
    -    public boolean canDelete(View view) {
    +    public boolean canDelete(@Nonnull View view) {
             return !view.isDefault();  // Cannot delete primary view
         }
     
    -    public synchronized void deleteView(View view) throws IOException {
    +    public synchronized void deleteView(@Nonnull View view) throws IOException {
             if (views().size() <= 1)
                 throw new IllegalStateException("Cannot delete last view");
             views().remove(view);
    @@ -101,11 +115,14 @@ public abstract class ViewGroupMixIn {
          */
         @CheckForNull
         public View getView(@CheckForNull String name) {
    +        if (name == null) {
    +            return null;
    +        }
             for (View v : views()) {
                 if(v.getViewName().equals(name))
                     return v;
             }
    -        if (name != null && !name.equals(primaryView())) {
    +        if (!name.equals(primaryView())) {
                 // Fallback to subview of primary view if it is a ViewGroup
                 View pv = getPrimaryView();
                 if (pv instanceof ViewGroup)
    diff --git a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java
    index 0a01a5f28fe5ca9cd329df55e244f87f76e2436e..8c55c2bc4d8b77b347d8ac3e012784b41f33bb34 100644
    --- a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java
    +++ b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java
    @@ -139,6 +139,15 @@ public class WorkspaceCleanupThread extends AsyncPeriodicWork {
                 }
             }
     
    +        // TODO this may only check the last build in fact:
    +        if (item instanceof Job<?,?>) {
    +            Job<?,?> j = (Job<?,?>) item;
    +            if (j.isBuilding()) {
    +                LOGGER.log(Level.FINE, "Job {0} is building, so not deleting", item.getFullDisplayName());
    +                return false;
    +            }
    +        }
    +
             LOGGER.log(Level.FINER, "Going to delete directory {0}", dir);
             return true;
         }
    diff --git a/core/src/main/java/hudson/model/listeners/ItemListener.java b/core/src/main/java/hudson/model/listeners/ItemListener.java
    index c48d4db8f447f66a587628c5251657dfd8c68e07..f72a8acff336381c0546798b71a363cacf1ef0e7 100644
    --- a/core/src/main/java/hudson/model/listeners/ItemListener.java
    +++ b/core/src/main/java/hudson/model/listeners/ItemListener.java
    @@ -64,7 +64,7 @@ public class ItemListener implements ExtensionPoint {
          * @param src the item being copied
          * @param parent the proposed parent
          * @throws Failure to veto the operation.
    -     * @since TODO
    +     * @since 2.51
          */
         public void onCheckCopy(Item src, ItemGroup parent) throws Failure {
         }
    @@ -200,7 +200,7 @@ public class ItemListener implements ExtensionPoint {
          * @param src    the item being copied
          * @param parent the proposed parent
          * @throws Failure if the copy operation has been vetoed.
    -     * @since TODO
    +     * @since 2.51
          */
         public static void checkBeforeCopy(final Item src, final ItemGroup parent) throws Failure {
             for (ItemListener l : all()) {
    diff --git a/core/src/main/java/hudson/model/package.html b/core/src/main/java/hudson/model/package.html
    index 640b328a391dfaf338c5b4e9ffd7dae8d679b5bb..73e34b62da6534d444ef2ca712f87b093383550b 100644
    --- a/core/src/main/java/hudson/model/package.html
    +++ b/core/src/main/java/hudson/model/package.html
    @@ -23,5 +23,5 @@ THE SOFTWARE.
     -->
     
     <html><head/><body>
    -Core object model that are bound to URLs via stapler, rooted at <a href="Hudson.html"><tt>Hudson</tt></a>.
    +Core object model that are bound to URLs via stapler, rooted at <a href="Hudson.html"><code>Hudson</code></a>.
     </body></html>
    \ No newline at end of file
    diff --git a/core/src/main/java/hudson/model/queue/BackFiller.java b/core/src/main/java/hudson/model/queue/BackFiller.java
    index 6278dd9b99e4cd43ef57b92fe11ff75d7eae0a2c..bc3773cc4d3cd558d46e193ce8fdaf4cc4f0a402 100644
    --- a/core/src/main/java/hudson/model/queue/BackFiller.java
    +++ b/core/src/main/java/hudson/model/queue/BackFiller.java
    @@ -12,7 +12,7 @@ import hudson.model.queue.MappingWorksheet.ExecutorChunk;
     import hudson.model.queue.MappingWorksheet.ExecutorSlot;
     import hudson.model.queue.MappingWorksheet.Mapping;
     import hudson.model.queue.MappingWorksheet.WorkChunk;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     
     import java.util.ArrayList;
     import java.util.Collections;
    @@ -124,7 +124,7 @@ public class BackFiller extends LoadPredictor {
                 // The downside of guessing the duration wrong is that we can end up creating tentative plans
                 // afterward that may be incorrect, but those plans will be rebuilt.
                 long d = bi.task.getEstimatedDuration();
    -            if (d<=0)    d = TimeUnit2.MINUTES.toMillis(5);
    +            if (d<=0)    d = TimeUnit.MINUTES.toMillis(5);
     
                 TimeRange slot = new TimeRange(System.currentTimeMillis(), d);
     
    diff --git a/core/src/main/java/hudson/model/queue/CauseOfBlockage.java b/core/src/main/java/hudson/model/queue/CauseOfBlockage.java
    index e5ba86359771f88092d1ed5982197bc48dad24d7..beb76b824a34b11729ea57fa3d71ab012d480b76 100644
    --- a/core/src/main/java/hudson/model/queue/CauseOfBlockage.java
    +++ b/core/src/main/java/hudson/model/queue/CauseOfBlockage.java
    @@ -19,7 +19,7 @@ import org.jvnet.localizer.Localizable;
      * has expanded beyond queues.
      *
      * <h2>View</h2>
    - * <tt>summary.jelly</tt> should do one-line HTML rendering to be used showing the cause
    + * {@code summary.jelly} should do one-line HTML rendering to be used showing the cause
      * to the user. By default it simply renders {@link #getShortDescription()} text.
      *
      * <p>
    diff --git a/core/src/main/java/hudson/model/queue/Executables.java b/core/src/main/java/hudson/model/queue/Executables.java
    index 2dbcca3f21bb04616dd79efc06229fabd6ad0590..426c12df96e5d3f2af55d15621b7a7c9b50eec9e 100644
    --- a/core/src/main/java/hudson/model/queue/Executables.java
    +++ b/core/src/main/java/hudson/model/queue/Executables.java
    @@ -46,7 +46,7 @@ public class Executables {
                 throws Error, RuntimeException {
             try {
                 return e.getParent();
    -        } catch (AbstractMethodError _) {
    +        } catch (AbstractMethodError ignored) { // will fallback to a private implementation
                 try {
                     Method m = e.getClass().getMethod("getParent");
                     m.setAccessible(true);
    diff --git a/core/src/main/java/hudson/model/queue/MappingWorksheet.java b/core/src/main/java/hudson/model/queue/MappingWorksheet.java
    index a8cdad8ef9e01847b5fc25db291877e84bdcc3bc..561c950e4aa3e17dcbe13aa65c928887b8238ca6 100644
    --- a/core/src/main/java/hudson/model/queue/MappingWorksheet.java
    +++ b/core/src/main/java/hudson/model/queue/MappingWorksheet.java
    @@ -134,7 +134,7 @@ public class MappingWorksheet {
                 if (c.assignedLabel!=null && !c.assignedLabel.contains(node))
                     return false;   // label mismatch
     
    -            if (!nodeAcl.hasPermission(item.authenticate(), Computer.BUILD))
    +            if (!(Node.SKIP_BUILD_CHECK_ON_FLYWEIGHTS && item.task instanceof Queue.FlyweightTask) && !nodeAcl.hasPermission(item.authenticate(), Computer.BUILD))
                     return false;   // tasks don't have a permission to run on this node
     
                 return true;
    diff --git a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java
    index 4688551b8e185ae277fde604351dfc7d3af2a717..9bf4887247da5a47d6cc0cf278938039b435c2bc 100644
    --- a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java
    +++ b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java
    @@ -56,10 +56,12 @@ public abstract class QueueTaskFilter implements Queue.Task {
             return base.getLastBuiltOn();
         }
     
    +    @Deprecated
         public boolean isBuildBlocked() {
             return base.isBuildBlocked();
         }
     
    +    @Deprecated
         public String getWhyBlocked() {
             return base.getWhyBlocked();
         }
    diff --git a/core/src/main/java/hudson/model/queue/Tasks.java b/core/src/main/java/hudson/model/queue/Tasks.java
    index 2ae56bcc17f7bd90d372343819e697384e318237..baf2f9714f3be718f03ab1c6860036800f87f1bc 100644
    --- a/core/src/main/java/hudson/model/queue/Tasks.java
    +++ b/core/src/main/java/hudson/model/queue/Tasks.java
    @@ -65,7 +65,7 @@ public class Tasks {
          * @param t the {@link SubTask}.
          * @return the {@link hudson.model.Item} associated with the {@link SubTask} or {@code null} if this
          * {@link SubTask} is not associated with an {@link hudson.model.Item}
    -     * @since TODO
    +     * @since 2.55
          */
         @CheckForNull
         public static hudson.model.Item getItemOf(@Nonnull SubTask t) {
    diff --git a/core/src/main/java/hudson/model/queue/WorkUnit.java b/core/src/main/java/hudson/model/queue/WorkUnit.java
    index 3b98d19a4b2523265893301aa5433e38814f6100..7665c42a0e1d4e91fb72edff4c592ad033906433 100644
    --- a/core/src/main/java/hudson/model/queue/WorkUnit.java
    +++ b/core/src/main/java/hudson/model/queue/WorkUnit.java
    @@ -31,7 +31,6 @@ import javax.annotation.CheckForNull;
     import hudson.model.Run;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
    -import org.kohsuke.stapler.export.ExportedBean;
     
     /**
      * Represents a unit of hand-over to {@link Executor} from {@link Queue}.
    @@ -39,7 +38,6 @@ import org.kohsuke.stapler.export.ExportedBean;
      * @author Kohsuke Kawaguchi
      * @since 1.377
      */
    -@ExportedBean
     public final class WorkUnit {
         /**
          * Task to be executed.
    diff --git a/core/src/main/java/hudson/model/queue/WorkUnitContext.java b/core/src/main/java/hudson/model/queue/WorkUnitContext.java
    index 80a402242d5112a6c0962647f70a76c3bffe7d4b..8afce5cbfd0f757816ef82966ad31354a612b68f 100644
    --- a/core/src/main/java/hudson/model/queue/WorkUnitContext.java
    +++ b/core/src/main/java/hudson/model/queue/WorkUnitContext.java
    @@ -67,8 +67,8 @@ public final class WorkUnitContext {
             this.item = item;
             this.task = item.task;
             this.future = (FutureImpl)item.getFuture();
    -        this.actions = new ArrayList<Action>(item.getAllActions());
    -        
    +        // JENKINS-51584 do not use item.getAllActions() here.
    +        this.actions = new ArrayList<Action>(item.getActions());
             // +1 for the main task
             int workUnitSize = task.getSubTasks().size();
             startLatch = new Latch(workUnitSize) {
    diff --git a/core/src/main/java/hudson/node_monitors/AbstractAsyncNodeMonitorDescriptor.java b/core/src/main/java/hudson/node_monitors/AbstractAsyncNodeMonitorDescriptor.java
    index 8621f37cebe47cd29ba7bceb3aa8b313224aafe9..e35ae80a7e133c7ff6ca20b3d1344b979ced0735 100644
    --- a/core/src/main/java/hudson/node_monitors/AbstractAsyncNodeMonitorDescriptor.java
    +++ b/core/src/main/java/hudson/node_monitors/AbstractAsyncNodeMonitorDescriptor.java
    @@ -6,10 +6,16 @@ import hudson.remoting.VirtualChannel;
     import jenkins.model.Jenkins;
     
     import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     import java.io.IOException;
    +import java.util.ArrayList;
    +import java.util.Collection;
     import java.util.HashMap;
    +import java.util.HashSet;
    +import java.util.List;
     import java.util.Map;
     import java.util.Map.Entry;
    +import java.util.Set;
     import java.util.concurrent.ExecutionException;
     import java.util.concurrent.Future;
     import java.util.concurrent.TimeoutException;
    @@ -60,10 +66,22 @@ public abstract class AbstractAsyncNodeMonitorDescriptor<T> extends AbstractNode
     
         /**
          * Performs all monitoring concurrently.
    +     *
    +     * @return Mapping from computer to monitored value. The map values can be null for several reasons, see {@link Result}
    +     * for more details.
          */
         @Override
         protected Map<Computer, T> monitor() throws InterruptedException {
    +        // Bridge method to offer original constrained interface.
    +        return monitorDetailed().getMonitoringData();
    +    }
    +
    +    /**
    +     * Perform monitoring with detailed reporting.
    +     */
    +    protected final @Nonnull Result<T> monitorDetailed() throws InterruptedException {
             Map<Computer,Future<T>> futures = new HashMap<Computer,Future<T>>();
    +        Set<Computer> skipped = new HashSet<>();
     
             for (Computer c : Jenkins.getInstance().getComputers()) {
                 try {
    @@ -101,11 +119,53 @@ public abstract class AbstractAsyncNodeMonitorDescriptor<T> extends AbstractNode
                     } catch (TimeoutException x) {
                         LOGGER.log(WARNING, "Failed to monitor " + c.getDisplayName() + " for " + getDisplayName(), x);
                     }
    +            } else {
    +                skipped.add(c);
                 }
             }
     
    -        return data;
    +        return new Result<>(data, skipped);
         }
     
         private static final Logger LOGGER = Logger.getLogger(AbstractAsyncNodeMonitorDescriptor.class.getName());
    +
    +    /**
    +     * Result object for {@link AbstractAsyncNodeMonitorDescriptor#monitorDetailed()} to facilitate extending information
    +     * returned in the future.
    +     *
    +     * The {@link #getMonitoringData()} provides the results of the monitoring as {@link #monitor()} does. Note the value
    +     * in the map can be {@code null} for several reasons:
    +     * <ul>
    +     *     <li>The monitoring {@link Callable} returned {@code null} as a provisioning result.</li>
    +     *     <li>Creating or evaluating that callable has thrown an exception.</li>
    +     *     <li>The computer was not monitored as it was offline.</li>
    +     *     <li>The {@link AbstractAsyncNodeMonitorDescriptor#createCallable} has returned null.</li>
    +     * </ul>
    +     *
    +     * Clients can distinguishing among these states based on the additional data attached to this object. {@link #getSkipped()}
    +     * returns computers that was not monitored as they ware either offline or monitor produced {@code null} {@link Callable}.
    +     */
    +    protected static final class Result<T> {
    +        private static final long serialVersionUID = -7671448355804481216L;
    +
    +        private final @Nonnull Map<Computer, T> data;
    +        private final @Nonnull ArrayList<Computer> skipped;
    +
    +        private Result(@Nonnull Map<Computer, T> data, @Nonnull Collection<Computer> skipped) {
    +            this.data = new HashMap<>(data);
    +            this.skipped = new ArrayList<>(skipped);
    +        }
    +
    +        protected @Nonnull Map<Computer, T> getMonitoringData() {
    +            return data;
    +        }
    +
    +        /**
    +         * Computers that ware skipped during monitoring as they either do not have a a channel (offline) or the monitor
    +         * have not produced the Callable. Computers that caused monitor to throw exception are not returned here.
    +         */
    +        protected @Nonnull List<Computer> getSkipped() {
    +            return skipped;
    +        }
    +    }
     }
    diff --git a/core/src/main/java/hudson/node_monitors/AbstractDiskSpaceMonitor.java b/core/src/main/java/hudson/node_monitors/AbstractDiskSpaceMonitor.java
    index 384133b93731a766f084b26f03c9166420ece12c..a8acfd78df5e52104d8279235c94e762e7d7a885 100644
    --- a/core/src/main/java/hudson/node_monitors/AbstractDiskSpaceMonitor.java
    +++ b/core/src/main/java/hudson/node_monitors/AbstractDiskSpaceMonitor.java
    @@ -47,7 +47,7 @@ public abstract class AbstractDiskSpaceMonitor extends NodeMonitor {
             if(size!=null && size.size > getThresholdBytes() && c.isOffline() && c.getOfflineCause() instanceof DiskSpace)
                 if(this.getClass().equals(((DiskSpace)c.getOfflineCause()).getTrigger()))
                     if(getDescriptor().markOnline(c)) {
    -                    LOGGER.warning(Messages.DiskSpaceMonitor_MarkedOnline(c.getName()));
    +                    LOGGER.info(Messages.DiskSpaceMonitor_MarkedOnline(c.getName()));
                     }
             return size;
         }
    diff --git a/core/src/main/java/hudson/node_monitors/NodeMonitor.java b/core/src/main/java/hudson/node_monitors/NodeMonitor.java
    index 17ddf7343ef29036b32c6eef2ebf26dcb07f59af..665b9cafe6dcec2219880eedc7c15954963472ec 100644
    --- a/core/src/main/java/hudson/node_monitors/NodeMonitor.java
    +++ b/core/src/main/java/hudson/node_monitors/NodeMonitor.java
    @@ -48,7 +48,7 @@ import org.kohsuke.stapler.export.ExportedBean;
      * <dl>
      * <dt>column.jelly</dt>
      * <dd>
    - * Invoked from {@link ComputerSet} <tt>index.jelly</tt> to render a column.
    + * Invoked from {@link ComputerSet} {@code index.jelly} to render a column.
      * The {@link NodeMonitor} instance is accessible through the "from" variable.
      * Also see {@link #getColumnCaption()}.
      *
    diff --git a/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java b/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java
    index 652b2217fbb142363ac76fc8c572c10023270287..9f77f9bb779e3ed08e0ddc750364fa4a2d5bc817 100644
    --- a/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java
    +++ b/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java
    @@ -23,7 +23,6 @@
      */
     package hudson.node_monitors;
     
    -import hudson.Util;
     import hudson.Extension;
     import hudson.model.Computer;
     import hudson.remoting.Callable;
    @@ -47,6 +46,7 @@ import org.kohsuke.stapler.export.ExportedBean;
     public class ResponseTimeMonitor extends NodeMonitor {
         @Extension
         public static final AbstractNodeMonitorDescriptor<Data> DESCRIPTOR = new AbstractAsyncNodeMonitorDescriptor<Data>() {
    +
             @Override
             protected Callable<Data,IOException> createCallable(Computer c) {
                 return new Step1(get(c));
    @@ -54,10 +54,16 @@ public class ResponseTimeMonitor extends NodeMonitor {
     
             @Override
             protected Map<Computer, Data> monitor() throws InterruptedException {
    -            Map<Computer, Data> base = super.monitor();
    -            for (Entry<Computer, Data> e : base.entrySet()) {
    +            Result<Data> base = monitorDetailed();
    +            Map<Computer, Data> monitoringData = base.getMonitoringData();
    +            for (Entry<Computer, Data> e : monitoringData.entrySet()) {
                     Computer c = e.getKey();
                     Data d = e.getValue();
    +                if (base.getSkipped().contains(c)) {
    +                    assert d == null;
    +                    continue;
    +                }
    +
                     if (d ==null) {
                         // if we failed to monitor, put in the special value that indicates a failure
                         e.setValue(d=new Data(get(c),-1L));
    @@ -72,7 +78,7 @@ public class ResponseTimeMonitor extends NodeMonitor {
                         LOGGER.warning(Messages.ResponseTimeMonitor_MarkedOffline(c.getName()));
                     }
                 }
    -            return base;
    +            return monitoringData;
             }
     
             public String getDisplayName() {
    diff --git a/core/src/main/java/hudson/os/solaris/ZFSInstaller.java b/core/src/main/java/hudson/os/solaris/ZFSInstaller.java
    index a5709e8976ad1b39b17cec622aaee0798f89190b..f22a0b7879d1f6441cf6afef0bc944a75d9b8308 100644
    --- a/core/src/main/java/hudson/os/solaris/ZFSInstaller.java
    +++ b/core/src/main/java/hudson/os/solaris/ZFSInstaller.java
    @@ -169,9 +169,23 @@ public class ZFSInstaller extends AdministrativeMonitor implements Serializable
     
             // this is the actual creation of the file system.
             // return true indicating a success
    -        return SU.execute(listener, rootUsername, rootPassword, new MasterToSlaveCallable<String,IOException>() {
    +        return SU.execute(listener, rootUsername, rootPassword, new Create(listener, home, uid, gid, userName));
    +    }
    +    private static class Create extends MasterToSlaveCallable<String, IOException> {
    +        private final TaskListener listener;
    +        private final File home;
    +        private final int uid;
    +        private final int gid;
    +        private final String userName;
    +        Create(TaskListener listener, File home, int uid, int gid, String userName) {
    +            this.listener = listener;
    +            this.home = home;
    +            this.uid = uid;
    +            this.gid = gid;
    +            this.userName = userName;
    +        }
                 private static final long serialVersionUID = 7731167233498214301L;
    -
    +            @Override
                 public String call() throws IOException {
                     PrintStream out = listener.getLogger();
     
    @@ -205,14 +219,13 @@ public class ZFSInstaller extends AdministrativeMonitor implements Serializable
                         // revert the file system creation
                         try {
                             hudson.destory();
    -                    } catch (Exception _) {
    +                    } catch (Exception ignored) {
                             // but ignore the error and let the original error thrown
                         }
                         throw e;
                     }
                     return hudson.getName();
                 }
    -        });
         }
     
         /**
    diff --git a/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java b/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java
    index 229b5affb54e7cb9404a0f0c246381e349a0c78d..aeb46745118cd5155753cb218e27ac9bf5d8e09d 100644
    --- a/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java
    +++ b/core/src/main/java/hudson/os/solaris/ZFSProvisioner.java
    @@ -53,9 +53,11 @@ public class ZFSProvisioner extends FileSystemProvisioner implements Serializabl
         private final String rootDataset;
     
         public ZFSProvisioner(Node node) throws IOException, InterruptedException {
    -        rootDataset = node.getRootPath().act(new MasterToSlaveFileCallable<String>() {
    +        rootDataset = node.getRootPath().act(new GetName());
    +    }
    +    private static class GetName extends MasterToSlaveFileCallable<String> {
                 private static final long serialVersionUID = -2142349338699797436L;
    -
    +            @Override
                 public String invoke(File f, VirtualChannel channel) throws IOException {
                     ZFSFileSystem fs = libzfs.getFileSystemByMountPoint(f);
                     if(fs!=null)    return fs.getName();
    @@ -63,15 +65,22 @@ public class ZFSProvisioner extends FileSystemProvisioner implements Serializabl
                     // TODO: for now, only support agents that are already on ZFS.
                     throw new IOException("Not on ZFS");
                 }
    -        });
         }
     
         public void prepareWorkspace(AbstractBuild<?,?> build, FilePath ws, final TaskListener listener) throws IOException, InterruptedException {
             final String name = build.getProject().getFullName();
             
    -        ws.act(new MasterToSlaveFileCallable<Void>() {
    +        ws.act(new PrepareWorkspace(name, listener));
    +    }
    +    private class PrepareWorkspace extends MasterToSlaveFileCallable<Void> {
    +        private final String name;
    +        private final TaskListener listener;
    +        PrepareWorkspace(String name, TaskListener listener) {
    +            this.name = name;
    +            this.listener = listener;
    +        }
                 private static final long serialVersionUID = 2129531727963121198L;
    -
    +            @Override
                 public Void invoke(File f, VirtualChannel channel) throws IOException {
                     ZFSFileSystem fs = libzfs.getFileSystemByMountPoint(f);
                     if(fs!=null)    return null;    // already on ZFS
    @@ -84,20 +93,20 @@ public class ZFSProvisioner extends FileSystemProvisioner implements Serializabl
                     fs.mount();
                     return null;
                 }
    -        });
         }
     
         public void discardWorkspace(AbstractProject<?, ?> project, FilePath ws) throws IOException, InterruptedException {
    -        ws.act(new MasterToSlaveFileCallable<Void>() {
    +        ws.act(new DiscardWorkspace());
    +    }
    +    private static class DiscardWorkspace extends MasterToSlaveFileCallable<Void> {
                 private static final long serialVersionUID = 1916618107019257530L;
    -
    +            @Override
                 public Void invoke(File f, VirtualChannel channel) throws IOException {
                     ZFSFileSystem fs = libzfs.getFileSystemByMountPoint(f);
                     if(fs!=null)
                         fs.destory(true);
                     return null;
                 }
    -        });
         }
     
         /**
    diff --git a/core/src/main/java/hudson/scheduler/CronTab.java b/core/src/main/java/hudson/scheduler/CronTab.java
    index 58e580a9440a7a3d3d3f8e19e9bf5b000be95603..77118499ad757d6b1840e3b142cd4b87da4502a8 100644
    --- a/core/src/main/java/hudson/scheduler/CronTab.java
    +++ b/core/src/main/java/hudson/scheduler/CronTab.java
    @@ -204,7 +204,7 @@ public final class CronTab {
             }
     
             void setTo(Calendar c, int i) {
    -            c.set(field,i-offset);
    +            c.set(field,Math.min(i-offset, c.getActualMaximum(field)));
             }
     
             void clear(Calendar c) {
    @@ -328,7 +328,7 @@ public final class CronTab {
          * This method modifies the given calendar and returns the same object.
          *
          * @throws RareOrImpossibleDateException if the date isn't hit in the 2 years after it indicates an impossible
    -     * (e.g. Jun 31) date, or at least a date too rare to be useful. This addresses JENKINS-41864 and was added in TODO
    +     * (e.g. Jun 31) date, or at least a date too rare to be useful. This addresses JENKINS-41864 and was added in 2.49
          */
         public Calendar ceil(Calendar cal) {
             Calendar twoYearsFuture = (Calendar) cal.clone();
    @@ -356,6 +356,14 @@ public final class CronTab {
                         continue OUTER;
                     } else {
                         f.setTo(cal,next);
    +                    //check if value was actually set
    +                    if (f.valueOf(cal) != next) {
    +                        // we need to roll over to the next field.
    +                        f.rollUp(cal, 1);
    +                        f.setTo(cal,f.first(this));
    +                        // since higher order field is affected by this, we need to restart from all over
    +                        continue OUTER;
    +                    }
                         if (f.redoAdjustmentIfModified)
                             continue OUTER; // when we modify DAY_OF_MONTH and DAY_OF_WEEK, do it all over from the top
                     }
    @@ -389,7 +397,7 @@ public final class CronTab {
          * This method modifies the given calendar and returns the same object.
          *
          * @throws RareOrImpossibleDateException if the date isn't hit in the 2 years before it indicates an impossible
    -     * (e.g. Jun 31) date, or at least a date too rare to be useful. This addresses JENKINS-41864 and was added in TODO
    +     * (e.g. Jun 31) date, or at least a date too rare to be useful. This addresses JENKINS-41864 and was added in 2.49
          */
         public Calendar floor(Calendar cal) {
             Calendar twoYearsAgo = (Calendar) cal.clone();
    @@ -537,7 +545,7 @@ public final class CronTab {
          * Returns the configured time zone, or null if none is configured
          *
          * @return the configured time zone, or null if none is configured
    -     * @since TODO
    +     * @since 2.54
          */
         @CheckForNull public TimeZone getTimeZone() {
             if (this.specTimezone == null) {
    diff --git a/core/src/main/java/hudson/scheduler/CronTabList.java b/core/src/main/java/hudson/scheduler/CronTabList.java
    index bb20834597023e05f5e1d63c93fef6576c167dff..eba4f007f80521915901d5537d961329fd67d97a 100644
    --- a/core/src/main/java/hudson/scheduler/CronTabList.java
    +++ b/core/src/main/java/hudson/scheduler/CronTabList.java
    @@ -108,9 +108,9 @@ public final class CronTabList {
                 if(lineNumber == 1 && line.startsWith("TZ=")) {
                     timezone = getValidTimezone(line.replace("TZ=",""));
                     if(timezone != null) {
    -                    LOGGER.log(Level.CONFIG, "cron with timezone {0}", timezone);
    +                    LOGGER.log(Level.CONFIG, "CRON with timezone {0}", timezone);
                     } else {
    -                    LOGGER.log(Level.CONFIG, "invalid timezone {0}", line);
    +                    throw new ANTLRException("Invalid or unsupported timezone '" + timezone + "'");
                     }
                     continue;
                 }
    diff --git a/core/src/main/java/hudson/scheduler/RareOrImpossibleDateException.java b/core/src/main/java/hudson/scheduler/RareOrImpossibleDateException.java
    index 7f2cf4e2cfec564549778cd80596da37f7d9df57..bd4d4831b7202a9b3be5da057adb250a6fa911e2 100644
    --- a/core/src/main/java/hudson/scheduler/RareOrImpossibleDateException.java
    +++ b/core/src/main/java/hudson/scheduler/RareOrImpossibleDateException.java
    @@ -35,7 +35,7 @@ import java.util.Calendar;
      * <p>This can typically have a few different reasons:</p>
      *
      * <ul>
    - *   <li>The date is impossible. For example, June 31 does never happen, so <tt>0 0 31 6 *</tt> will never happen</li>
    + *   <li>The date is impossible. For example, June 31 does never happen, so {@code 0 0 31 6 *} will never happen</li>
      *   <li>The date happens only rarely
      *     <ul>
      *       <li>February 29 being the obvious one</li>
    diff --git a/core/src/main/java/hudson/scm/AbstractScmTagAction.java b/core/src/main/java/hudson/scm/AbstractScmTagAction.java
    index c098427ac6de87bf52cdef32b23ab18010a6e8fe..a86ba18382ec498bab920ff0b1256dd13346fed3 100644
    --- a/core/src/main/java/hudson/scm/AbstractScmTagAction.java
    +++ b/core/src/main/java/hudson/scm/AbstractScmTagAction.java
    @@ -37,11 +37,11 @@ import java.io.IOException;
     import jenkins.model.RunAction2;
     
     /**
    - * Common part of <tt>CVSSCM.TagAction</tt> and <tt>SubversionTagAction</tt>.
    + * Common part of {@code CVSSCM.TagAction} and {@code SubversionTagAction}.
      *
      * <p>
      * This class implements the action that tags the modules. Derived classes
    - * need to provide <tt>tagForm.jelly</tt> view that displays a form for
    + * need to provide {@code tagForm.jelly} view that displays a form for
      * letting user start tagging.
      *
      * @author Kohsuke Kawaguchi
    diff --git a/core/src/main/java/hudson/scm/NullChangeLogParser.java b/core/src/main/java/hudson/scm/NullChangeLogParser.java
    index 19f6aeceb781190449f46915c95147fdb1330672..d78a03cce3e66ea80fd3c79b0b7c320d5bbb4779 100644
    --- a/core/src/main/java/hudson/scm/NullChangeLogParser.java
    +++ b/core/src/main/java/hudson/scm/NullChangeLogParser.java
    @@ -41,7 +41,7 @@ public class NullChangeLogParser extends ChangeLogParser {
             return ChangeLogSet.createEmpty(build);
         }
         
    -    public Object readResolve() {
    +    protected Object readResolve() {
             return INSTANCE;
         }
     }
    diff --git a/core/src/main/java/hudson/scm/SCM.java b/core/src/main/java/hudson/scm/SCM.java
    index 73711dd4cbd0a2fe353569de5ebf63184ab6be2f..b49f3f10fbcd6a11aa35d1d2bee223a6a914e1e8 100644
    --- a/core/src/main/java/hudson/scm/SCM.java
    +++ b/core/src/main/java/hudson/scm/SCM.java
    @@ -54,6 +54,8 @@ import java.io.IOException;
     import java.util.ArrayList;
     import java.util.List;
     import java.util.Map;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
     import javax.annotation.Nullable;
    @@ -86,6 +88,8 @@ import org.kohsuke.stapler.export.ExportedBean;
     @ExportedBean
     public abstract class SCM implements Describable<SCM>, ExtensionPoint {
     
    +    private static final Logger LOGGER = Logger.getLogger(SCM.class.getName());
    +
         /** JENKINS-35098: discouraged */
         @SuppressWarnings("FieldMayBeFinal")
         private static boolean useAutoBrowserHolder = SystemProperties.getBoolean(SCM.class.getName() + ".useAutoBrowserHolder");
    @@ -143,7 +147,12 @@ public abstract class SCM implements Describable<SCM>, ExtensionPoint {
                 }
                 return autoBrowserHolder.get();
             } else {
    -            return guessBrowser();
    +            try {
    +                return guessBrowser();
    +            } catch (RuntimeException x) {
    +                LOGGER.log(Level.WARNING, null, x);
    +                return null;
    +            }
             }
         }
     
    @@ -527,7 +536,7 @@ public abstract class SCM implements Describable<SCM>, ExtensionPoint {
          * are going to provide information about check out (like SVN revision number that was checked out), be prepared
          * for the possibility that the check out hasn't happened yet.
          *
    -     * @since FIXME
    +     * @since 2.60
          */
         public void buildEnvironment(@Nonnull Run<?,?> build, @Nonnull Map<String,String> env) {
             if (build instanceof AbstractBuild) {
    @@ -535,6 +544,9 @@ public abstract class SCM implements Describable<SCM>, ExtensionPoint {
             }
         }
     
    +    /**
    +     * @deprecated in favor of {@link #buildEnvironment(Run, Map)}.
    +     */
         @Deprecated
         public void buildEnvVars(AbstractBuild<?,?> build, Map<String, String> env) {
             if (Util.isOverridden(SCM.class, getClass(), "buildEnvironment", Run.class, Map.class)) {
    @@ -560,7 +572,7 @@ public abstract class SCM implements Describable<SCM>, ExtensionPoint {
          * <p>
          * Many builders, like Ant or Maven, works off the specific user file
          * at the top of the checked out module (in the above case, that would
    -     * be <tt>xyz/build.xml</tt>), yet the builder doesn't know the "xyz"
    +     * be {@code xyz/build.xml}), yet the builder doesn't know the "xyz"
          * part; that comes from SCM.
          *
          * <p>
    @@ -666,7 +678,7 @@ public abstract class SCM implements Describable<SCM>, ExtensionPoint {
         }
     
         /**
    -     * The returned object will be used to parse <tt>changelog.xml</tt>.
    +     * The returned object will be used to parse {@code changelog.xml}.
          */
         public abstract ChangeLogParser createChangeLogParser();
     
    diff --git a/core/src/main/java/hudson/scm/package.html b/core/src/main/java/hudson/scm/package.html
    index 610f57991d9931e3ee1ea5e658fafa90b5d076ca..d2a0046c665ff9e2217f5ec12e5023989df06616 100644
    --- a/core/src/main/java/hudson/scm/package.html
    +++ b/core/src/main/java/hudson/scm/package.html
    @@ -23,5 +23,5 @@ THE SOFTWARE.
     -->
     
     <html><head/><body>
    -Hudson's interface with source code management systems. Start with <a href="SCM.html"><tt>SCM</tt></a>
    +Hudson's interface with source code management systems. Start with <a href="SCM.html"><code>SCM</code></a>
     </body></html>
    \ No newline at end of file
    diff --git a/core/src/main/java/hudson/search/FixedSet.java b/core/src/main/java/hudson/search/FixedSet.java
    index f9ab1ade2990e273beac460e43ca4a8088b1d37d..eca310184329baf7a1925f447596501f7d89d2e2 100644
    --- a/core/src/main/java/hudson/search/FixedSet.java
    +++ b/core/src/main/java/hudson/search/FixedSet.java
    @@ -50,7 +50,7 @@ public class FixedSet implements SearchIndex {
             boolean caseInsensitive = UserSearchProperty.isCaseInsensitive();
             for (SearchItem i : items) {
                 String name = i.getSearchName();
    -            if (name.equals(token) || (caseInsensitive && name.equalsIgnoreCase(token))) {
    +            if (name != null && (name.equals(token) || (caseInsensitive && name.equalsIgnoreCase(token)))) {
                     result.add(i);
                 }
             }
    @@ -60,7 +60,7 @@ public class FixedSet implements SearchIndex {
             boolean caseInsensitive = UserSearchProperty.isCaseInsensitive();
             for (SearchItem i : items) {
                 String name = i.getSearchName();
    -            if (name.contains(token) || (caseInsensitive && StringUtils.containsIgnoreCase(name, token))) {
    +            if (name != null && (name.contains(token) || (caseInsensitive && StringUtils.containsIgnoreCase(name, token)))) {
                     result.add(i);
                 }
             }
    diff --git a/core/src/main/java/hudson/search/Search.java b/core/src/main/java/hudson/search/Search.java
    index 627d77fafba4c570c86c12bac722b895764a3822..ba743c75552caa4974e1e6977f330293ab03b3ce 100644
    --- a/core/src/main/java/hudson/search/Search.java
    +++ b/core/src/main/java/hudson/search/Search.java
    @@ -40,12 +40,16 @@ import java.util.Set;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     
    +import javax.annotation.CheckForNull;
     import javax.servlet.ServletException;
     
    +import jenkins.util.MemoryReductionUtil;
    +import jenkins.model.Jenkins;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.Ancestor;
     import org.kohsuke.stapler.QueryParameter;
    +import org.kohsuke.stapler.StaplerProxy;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.export.DataWriter;
    @@ -63,7 +67,7 @@ import org.kohsuke.stapler.export.Flavor;
      * @author Kohsuke Kawaguchi
      * @see SearchableModelObject
      */
    -public class Search {
    +public class Search implements StaplerProxy {
         @Restricted(NoExternalUse.class) // used from stapler views only
         public static String encodeQuery(String query) throws UnsupportedEncodingException {
             return URLEncoder.encode(query, "UTF-8");
    @@ -140,7 +144,7 @@ public class Search {
         public SearchResult getSuggestions(StaplerRequest req, String query) {
             Set<String> paths = new HashSet<String>();  // paths already added, to control duplicates
             SearchResultImpl r = new SearchResultImpl();
    -        int max = req.hasParameter("max") ? Integer.parseInt(req.getParameter("max")) : 20;
    +        int max = req.hasParameter("max") ? Integer.parseInt(req.getParameter("max")) : 100;
             SearchableModelObject smo = findClosestSearchableModelObject(req);
             for (SuggestedItem i : suggest(makeSuggestIndex(req), query, smo)) {
                 if(r.size()>=max) {
    @@ -153,7 +157,7 @@ public class Search {
             return r;
         }
     
    -    private SearchableModelObject findClosestSearchableModelObject(StaplerRequest req) {
    +    private @CheckForNull SearchableModelObject findClosestSearchableModelObject(StaplerRequest req) {
             List<Ancestor> l = req.getAncestors();
             for( int i=l.size()-1; i>=0; i-- ) {
                 Ancestor a = l.get(i);
    @@ -323,16 +327,14 @@ public class Search {
         static final class TokenList {
             private final String[] tokens;
     
    -        private final static String[] EMPTY = new String[0];
    -
             public TokenList(String tokenList) {
    -            tokens = tokenList!=null ? tokenList.split("(?<=\\s)(?=\\S)") : EMPTY;
    +            tokens = tokenList!=null ? tokenList.split("(?<=\\s)(?=\\S)") : MemoryReductionUtil.EMPTY_STRING_ARRAY;
             }
     
             public int length() { return tokens.length; }
     
             /**
    -         * Returns {@link List} such that its <tt>get(end)</tt>
    +         * Returns {@link List} such that its {@code get(end)}
              * returns the concatenation of [token_start,...,token_end]
              * (both end inclusive.)
              */
    @@ -406,6 +408,21 @@ public class Search {
     
             return paths[tokens.length()];
         }
    -    
    +
    +    @Override
    +    @Restricted(NoExternalUse.class)
    +    public Object getTarget() {
    +        if (!SKIP_PERMISSION_CHECK) {
    +            Jenkins.getInstance().checkPermission(Jenkins.READ);
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Escape hatch for StaplerProxy-based access control
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(Search.class.getName() + ".skipPermissionCheck");
    +
         private final static Logger LOGGER = Logger.getLogger(Search.class.getName());
     }
    diff --git a/core/src/main/java/hudson/security/ACL.java b/core/src/main/java/hudson/security/ACL.java
    index a2c463d97cad1fb6dca3ca003d48aa4830a9ea11..1f39620c66ebcc353f4f32772cba9cbfa5063744 100644
    --- a/core/src/main/java/hudson/security/ACL.java
    +++ b/core/src/main/java/hudson/security/ACL.java
    @@ -34,6 +34,7 @@ import hudson.model.Item;
     import hudson.remoting.Callable;
     import hudson.model.ItemGroup;
     import hudson.model.TopLevelItemDescriptor;
    +import java.util.function.BiFunction;
     import jenkins.security.NonSerializableSecurityContext;
     import jenkins.model.Jenkins;
     import jenkins.security.NotReallyRoleSensitiveCallable;
    @@ -44,6 +45,7 @@ import org.acegisecurity.context.SecurityContextHolder;
     import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
     import org.acegisecurity.acls.sid.PrincipalSid;
     import org.acegisecurity.acls.sid.Sid;
    +import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     
    @@ -64,6 +66,9 @@ public abstract class ACL {
          */
         public final void checkPermission(@Nonnull Permission p) {
             Authentication a = Jenkins.getAuthentication();
    +        if (a == SYSTEM) {
    +            return;
    +        }
             if(!hasPermission(a,p))
                 throw new AccessDeniedException2(a,p);
         }
    @@ -75,7 +80,11 @@ public abstract class ACL {
          *      if the user doesn't have the permission.
          */
         public final boolean hasPermission(@Nonnull Permission p) {
    -        return hasPermission(Jenkins.getAuthentication(),p);
    +        Authentication a = Jenkins.getAuthentication();
    +        if (a == SYSTEM) {
    +            return true;
    +        }
    +        return hasPermission(a, p);
         }
     
         /**
    @@ -87,6 +96,21 @@ public abstract class ACL {
          */
         public abstract boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission);
     
    +    /**
    +     * Creates a simple {@link ACL} implementation based on a “single-abstract-method” easily implemented via lambda syntax.
    +     * @param impl the implementation of {@link ACL#hasPermission(Authentication, Permission)}
    +     * @return an adapter to that lambda
    +     * @since 2.105
    +     */
    +    public static ACL lambda(final BiFunction<Authentication, Permission, Boolean> impl) {
    +        return new ACL() {
    +            @Override
    +            public boolean hasPermission(Authentication a, Permission permission) {
    +                return impl.apply(a, permission);
    +            }
    +        };
    +    }
    +
         /**
          * Checks if the current security principal has the permission to create top level items within the specified
          * item group.
    @@ -101,6 +125,9 @@ public abstract class ACL {
         public final void checkCreatePermission(@Nonnull ItemGroup c,
                                                 @Nonnull TopLevelItemDescriptor d) {
             Authentication a = Jenkins.getAuthentication();
    +        if (a == SYSTEM) {
    +            return;
    +        }
             if (!hasCreatePermission(a, c, d)) {
                 throw new AccessDeniedException(Messages.AccessDeniedException2_MissingPermission(a.getName(),
                         Item.CREATE.group.title+"/"+Item.CREATE.name + Item.CREATE + "/" + d.getDisplayName()));
    @@ -136,6 +163,9 @@ public abstract class ACL {
         public final void checkCreatePermission(@Nonnull ViewGroup c,
                                                 @Nonnull ViewDescriptor d) {
             Authentication a = Jenkins.getAuthentication();
    +        if (a == SYSTEM) {
    +            return;
    +        }
             if (!hasCreatePermission(a, c, d)) {
                 throw new AccessDeniedException(Messages.AccessDeniedException2_MissingPermission(a.getName(),
                         View.CREATE.group.title + "/" + View.CREATE.name + View.CREATE + "/" + d.getDisplayName()));
    @@ -306,4 +336,13 @@ public abstract class ACL {
             return as(user == null ? Jenkins.ANONYMOUS : user.impersonate());
         }
     
    +    /**
    +     * Checks if the given authentication is anonymous by checking its class.
    +     * @see Jenkins#ANONYMOUS
    +     * @see AnonymousAuthenticationToken
    +     */
    +    public static boolean isAnonymous(@Nonnull Authentication authentication) {
    +        //TODO use AuthenticationTrustResolver instead to be consistent through the application
    +        return authentication instanceof AnonymousAuthenticationToken;
    +    }
     }
    diff --git a/core/src/main/java/hudson/security/AccessControlled.java b/core/src/main/java/hudson/security/AccessControlled.java
    index 0b3e81adde90c7664ed7927b9fc017f6a281f87a..9aa084df2b0894d295145c26efdc0f719449ffec 100644
    --- a/core/src/main/java/hudson/security/AccessControlled.java
    +++ b/core/src/main/java/hudson/security/AccessControlled.java
    @@ -25,6 +25,7 @@ package hudson.security;
     
     import javax.annotation.Nonnull;
     import org.acegisecurity.AccessDeniedException;
    +import org.acegisecurity.Authentication;
     
     /**
      * Object that has an {@link ACL}
    @@ -42,11 +43,26 @@ public interface AccessControlled {
         /**
          * Convenient short-cut for {@code getACL().checkPermission(permission)}
          */
    -    void checkPermission(@Nonnull Permission permission) throws AccessDeniedException;
    +    default void checkPermission(@Nonnull Permission permission) throws AccessDeniedException {
    +        getACL().checkPermission(permission);
    +    }
     
         /**
          * Convenient short-cut for {@code getACL().hasPermission(permission)}
          */
    -    boolean hasPermission(@Nonnull Permission permission);
    +    default boolean hasPermission(@Nonnull Permission permission) {
    +        return getACL().hasPermission(permission);
    +    }
    +
    +    /**
    +     * Convenient short-cut for {@code getACL().hasPermission(a, permission)}
    +     * @since 2.92
    +     */
    +    default boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) {
    +        if (a == ACL.SYSTEM) {
    +            return true;
    +        }
    +        return getACL().hasPermission(a, permission);
    +    }
     
     }
    diff --git a/core/src/main/resources/jenkins/security/ApiTokenProperty/config.groovy b/core/src/main/java/hudson/security/AccountCreationFailedException.java
    similarity index 69%
    rename from core/src/main/resources/jenkins/security/ApiTokenProperty/config.groovy
    rename to core/src/main/java/hudson/security/AccountCreationFailedException.java
    index 76ab6b5eb5d029557190897fcaca604c62a11cbd..bab5345db1647133d7604a1b48ca0863d74437a0 100644
    --- a/core/src/main/resources/jenkins/security/ApiTokenProperty/config.groovy
    +++ b/core/src/main/java/hudson/security/AccountCreationFailedException.java
    @@ -1,7 +1,7 @@
     /*
      * The MIT License
      *
    - * Copyright (c) 2011, CloudBees, Inc.
    + * Copyright (c) 2017 Jenkins contributors
      *
      * Permission is hereby granted, free of charge, to any person obtaining a copy
      * of this software and associated documentation files (the "Software"), to deal
    @@ -21,16 +21,20 @@
      * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
      * THE SOFTWARE.
      */
    -package jenkins.security.ApiTokenProperty;
     
    -f=namespace(lib.FormTagLib)
    +package hudson.security;
     
    -f.advanced(title:_("Show API Token"), align:"left") {
    -    f.entry(title: _('User ID')) {
    -        f.readOnlyTextbox(value: my.id)
    -    }
    -    f.entry(title:_("API Token"), field:"apiToken") {
    -        f.readOnlyTextbox(id:"apiToken") // TODO: need to figure out the way to do this without using ID.
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +
    +/**
    + * Thrown if an account creation was attempted but failed due to invalid data being entered into a form.
    + *
    + * @author Philipp Nowak
    + */
    +@Restricted(NoExternalUse.class)
    +public class AccountCreationFailedException extends Exception {
    +    public AccountCreationFailedException(String message) {
    +        super(message);
         }
    -    f.validateButton(title:_("Change API Token"),method:"changeToken")
     }
    diff --git a/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java b/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
    index bda002cf106fd51e1c09345ae1b3f160759e3466..e1a281b482149d97d9079077d821bfd08ec9ebac 100644
    --- a/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
    +++ b/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
    @@ -30,16 +30,19 @@ import java.io.IOException;
     
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
    +import javax.servlet.http.HttpSession;
     
     import hudson.Util;
    +import hudson.model.User;
     import jenkins.security.SecurityListener;
    +import jenkins.security.seed.UserSeedProperty;
     import org.acegisecurity.Authentication;
     import org.acegisecurity.AuthenticationException;
     import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
     
     /**
      * {@link AuthenticationProcessingFilter} with a change for Jenkins so that
    - * we can pick up the hidden "from" form field defined in <tt>login.jelly</tt>
    + * we can pick up the hidden "from" form field defined in {@code login.jelly}
      * to send the user back to where he came from, after a successful authentication.
      * 
      * @author Kohsuke Kawaguchi
    @@ -87,7 +90,19 @@ public class AuthenticationProcessingFilter2 extends AuthenticationProcessingFil
             // (either when a redirect is issued, via its HttpResponseWrapper, or when the execution returns to its
             // doFilter method.
             request.getSession().invalidate();
    -        request.getSession();
    +        HttpSession newSession = request.getSession();
    +
    +        if (!UserSeedProperty.DISABLE_USER_SEED) {
    +            User user = User.getById(authResult.getName(), true);
    +
    +            UserSeedProperty userSeed = user.getProperty(UserSeedProperty.class);
    +            String sessionSeed = userSeed.getSeed();
    +            newSession.setAttribute(UserSeedProperty.USER_SESSION_SEED, sessionSeed);
    +        }
    +
    +        // as the request comes from Acegi redirect, that's not a Stapler one
    +        // thus it's not possible to retrieve it in the SecurityListener in that case
    +        // for that reason we need to keep the above code that apply quite the same logic as UserSeedSecurityListener
             SecurityListener.fireLoggedIn(authResult.getName());
         }
     
    diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java
    index a875908f212d7ec472f589dad9a27f93090e9260..4c246a40d4f63ed92cc400c3e97f586843855a68 100644
    --- a/core/src/main/java/hudson/security/AuthorizationStrategy.java
    +++ b/core/src/main/java/hudson/security/AuthorizationStrategy.java
    @@ -36,6 +36,7 @@ import java.util.Collections;
     import javax.annotation.Nonnull;
     
     import jenkins.model.Jenkins;
    +import jenkins.security.stapler.StaplerAccessibleType;
     import net.sf.json.JSONObject;
     
     import org.acegisecurity.Authentication;
    @@ -62,6 +63,7 @@ import org.kohsuke.stapler.StaplerRequest;
      * @author Kohsuke Kawaguchi
      * @see SecurityRealm
      */
    +@StaplerAccessibleType
     public abstract class AuthorizationStrategy extends AbstractDescribableImpl<AuthorizationStrategy> implements ExtensionPoint {
         /**
          * Returns the instance of {@link ACL} where all the other {@link ACL} instances
    @@ -95,9 +97,7 @@ public abstract class AuthorizationStrategy extends AbstractDescribableImpl<Auth
          * @since 1.220
          */
         public @Nonnull ACL getACL(final @Nonnull View item) {
    -        return new ACL() {
    -            @Override
    -            public boolean hasPermission(Authentication a, Permission permission) {
    +        return ACL.lambda((a, permission) -> {
                     ACL base = item.getOwner().getACL();
     
                     boolean hasPermission = base.hasPermission(a, permission);
    @@ -106,8 +106,7 @@ public abstract class AuthorizationStrategy extends AbstractDescribableImpl<Auth
                     }
     
                     return hasPermission;
    -            }
    -        };
    +        });
         }
         
         /**
    @@ -225,12 +224,7 @@ public abstract class AuthorizationStrategy extends AbstractDescribableImpl<Auth
                 return Collections.emptySet();
             }
     
    -        private static final ACL UNSECURED_ACL = new ACL() {
    -            @Override
    -            public boolean hasPermission(Authentication a, Permission permission) {
    -                return true;
    -            }
    -        };
    +        private static final ACL UNSECURED_ACL = ACL.lambda((a, p) -> true);
     
             @Extension @Symbol("unsecured")
             public static final class DescriptorImpl extends Descriptor<AuthorizationStrategy> {
    diff --git a/core/src/main/java/hudson/security/BCrypt.java b/core/src/main/java/hudson/security/BCrypt.java
    deleted file mode 100644
    index aebb6059a53050cbfe2fd7956bf4a0403b738f33..0000000000000000000000000000000000000000
    --- a/core/src/main/java/hudson/security/BCrypt.java
    +++ /dev/null
    @@ -1,777 +0,0 @@
    -// Copyright (c) 2006 Damien Miller <djm@mindrot.org>
    -//
    -// Permission to use, copy, modify, and distribute this software for any
    -// purpose with or without fee is hereby granted, provided that the above
    -// copyright notice and this permission notice appear in all copies.
    -//
    -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    -// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    -// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    -
    -package hudson.security; // repackaged from http://www.mindrot.org/files/jBCrypt/jBCrypt-0.4.zip (and public modifier removed)
    -
    -import java.io.UnsupportedEncodingException;
    -import java.security.SecureRandom;
    -
    -/**
    - * BCrypt implements OpenBSD-style Blowfish password hashing using
    - * the scheme described in "A Future-Adaptable Password Scheme" by
    - * Niels Provos and David Mazieres.
    - * <p>
    - * This password hashing system tries to thwart off-line password
    - * cracking using a computationally-intensive hashing algorithm,
    - * based on Bruce Schneier's Blowfish cipher. The work factor of
    - * the algorithm is parameterised, so it can be increased as
    - * computers get faster.
    - * <p>
    - * Usage is really simple. To hash a password for the first time,
    - * call the hashpw method with a random salt, like this:
    - * <p>
    - * <code>
    - * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt()); <br />
    - * </code>
    - * <p>
    - * To check whether a plaintext password matches one that has been
    - * hashed previously, use the checkpw method:
    - * <p>
    - * <code>
    - * if (BCrypt.checkpw(candidate_password, stored_hash))<br />
    - * &nbsp;&nbsp;&nbsp;&nbsp;System.out.println("It matches");<br />
    - * else<br />
    - * &nbsp;&nbsp;&nbsp;&nbsp;System.out.println("It does not match");<br />
    - * </code>
    - * <p>
    - * The gensalt() method takes an optional parameter (log_rounds)
    - * that determines the computational complexity of the hashing:
    - * <p>
    - * <code>
    - * String strong_salt = BCrypt.gensalt(10)<br />
    - * String stronger_salt = BCrypt.gensalt(12)<br />
    - * </code>
    - * <p>
    - * The amount of work increases exponentially (2**log_rounds), so
    - * each increment is twice as much work. The default log_rounds is
    - * 10, and the valid range is 4 to 30.
    - *
    - * @author Damien Miller
    - * @version 0.2
    - */
    -class BCrypt {
    -	// BCrypt parameters
    -	private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;
    -	private static final int BCRYPT_SALT_LEN = 16;
    -
    -	// Blowfish parameters
    -	private static final int BLOWFISH_NUM_ROUNDS = 16;
    -
    -	// Initial contents of key schedule
    -	private static final int P_orig[] = {
    -		0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
    -		0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
    -		0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
    -		0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
    -		0x9216d5d9, 0x8979fb1b
    -	};
    -	private static final int S_orig[] = {
    -		0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
    -		0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
    -		0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
    -		0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
    -		0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
    -		0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
    -		0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
    -		0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
    -		0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
    -		0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
    -		0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
    -		0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
    -		0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
    -		0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
    -		0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
    -		0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
    -		0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
    -		0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
    -		0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
    -		0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
    -		0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
    -		0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
    -		0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
    -		0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
    -		0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
    -		0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
    -		0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
    -		0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
    -		0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
    -		0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
    -		0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
    -		0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
    -		0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
    -		0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
    -		0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
    -		0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
    -		0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
    -		0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
    -		0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
    -		0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
    -		0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
    -		0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
    -		0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
    -		0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
    -		0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
    -		0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
    -		0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
    -		0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
    -		0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
    -		0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
    -		0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
    -		0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
    -		0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
    -		0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
    -		0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
    -		0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
    -		0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
    -		0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
    -		0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
    -		0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
    -		0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
    -		0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
    -		0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
    -		0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
    -		0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
    -		0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
    -		0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
    -		0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
    -		0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
    -		0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
    -		0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
    -		0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
    -		0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
    -		0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
    -		0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
    -		0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
    -		0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
    -		0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
    -		0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
    -		0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
    -		0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
    -		0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
    -		0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
    -		0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
    -		0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
    -		0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
    -		0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
    -		0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
    -		0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
    -		0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
    -		0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
    -		0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
    -		0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
    -		0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
    -		0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
    -		0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
    -		0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
    -		0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
    -		0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
    -		0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
    -		0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
    -		0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
    -		0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
    -		0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
    -		0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
    -		0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
    -		0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
    -		0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
    -		0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
    -		0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
    -		0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
    -		0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
    -		0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
    -		0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
    -		0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
    -		0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
    -		0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
    -		0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
    -		0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
    -		0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
    -		0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
    -		0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
    -		0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
    -		0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
    -		0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
    -		0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
    -		0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
    -		0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
    -		0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
    -		0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
    -		0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
    -		0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
    -		0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
    -		0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
    -		0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
    -		0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
    -		0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
    -		0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
    -		0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
    -		0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
    -		0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
    -		0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
    -		0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
    -		0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
    -		0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
    -		0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
    -		0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
    -		0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
    -		0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
    -		0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
    -		0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
    -		0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
    -		0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
    -		0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
    -		0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
    -		0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
    -		0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
    -		0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
    -		0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
    -		0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
    -		0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
    -		0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
    -		0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
    -		0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
    -		0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
    -		0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
    -		0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
    -		0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
    -		0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
    -		0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
    -		0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
    -		0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
    -		0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
    -		0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
    -		0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
    -		0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
    -		0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
    -		0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
    -		0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
    -		0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
    -		0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
    -		0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
    -		0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
    -		0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
    -		0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
    -		0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
    -		0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
    -		0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
    -		0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
    -		0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
    -		0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
    -		0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
    -		0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
    -		0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
    -		0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
    -		0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
    -		0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
    -		0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
    -		0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
    -		0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
    -		0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
    -		0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
    -		0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
    -		0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
    -		0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
    -		0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
    -		0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
    -		0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
    -		0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
    -		0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
    -		0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
    -		0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
    -		0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
    -		0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
    -		0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
    -		0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
    -		0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
    -		0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
    -		0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
    -		0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
    -		0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
    -		0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
    -		0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
    -		0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
    -		0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
    -		0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
    -		0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
    -		0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
    -		0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
    -		0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
    -		0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
    -		0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
    -		0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
    -		0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
    -		0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
    -		0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
    -		0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
    -		0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
    -		0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
    -		0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
    -		0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
    -		0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
    -		0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
    -		0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
    -		0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
    -		0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
    -		0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
    -		0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
    -		0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
    -		0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
    -		0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
    -		0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
    -		0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
    -		0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
    -		0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
    -		0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
    -	};
    -
    -	// bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls
    -	// this "ciphertext", but it is really plaintext or an IV. We keep
    -	// the name to make code comparison easier.
    -	static private final int bf_crypt_ciphertext[] = {
    -		0x4f727068, 0x65616e42, 0x65686f6c,
    -		0x64657253, 0x63727944, 0x6f756274
    -	};
    -
    -	// Table for Base64 encoding
    -	static private final char base64_code[] = {
    -		'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
    -		'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
    -		'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
    -		'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
    -		'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
    -		'6', '7', '8', '9'
    -	};
    -
    -	// Table for Base64 decoding
    -	static private final byte index_64[] = {
    -		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -		-1, -1, -1, -1, -1, -1, 0, 1, 54, 55,
    -		56, 57, 58, 59, 60, 61, 62, 63, -1, -1,
    -		-1, -1, -1, -1, -1, 2, 3, 4, 5, 6,
    -		7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
    -		17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
    -		-1, -1, -1, -1, -1, -1, 28, 29, 30,
    -		31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    -		41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
    -		51, 52, 53, -1, -1, -1, -1, -1
    -	};
    -
    -	// Expanded Blowfish key
    -	private int P[];
    -	private int S[];
    -
    -	/**
    -	 * Encode a byte array using bcrypt's slightly-modified base64
    -	 * encoding scheme. Note that this is *not* compatible with
    -	 * the standard MIME-base64 encoding.
    -	 *
    -	 * @param d	the byte array to encode
    -	 * @param len	the number of bytes to encode
    -	 * @return	base64-encoded string
    -	 * @exception IllegalArgumentException if the length is invalid
    -	 */
    -	private static String encode_base64(byte d[], int len)
    -		throws IllegalArgumentException {
    -		int off = 0;
    -		StringBuffer rs = new StringBuffer();
    -		int c1, c2;
    -
    -		if (len <= 0 || len > d.length)
    -			throw new IllegalArgumentException ("Invalid len");
    -
    -		while (off < len) {
    -			c1 = d[off++] & 0xff;
    -			rs.append(base64_code[(c1 >> 2) & 0x3f]);
    -			c1 = (c1 & 0x03) << 4;
    -			if (off >= len) {
    -				rs.append(base64_code[c1 & 0x3f]);
    -				break;
    -			}
    -			c2 = d[off++] & 0xff;
    -			c1 |= (c2 >> 4) & 0x0f;
    -			rs.append(base64_code[c1 & 0x3f]);
    -			c1 = (c2 & 0x0f) << 2;
    -			if (off >= len) {
    -				rs.append(base64_code[c1 & 0x3f]);
    -				break;
    -			}
    -			c2 = d[off++] & 0xff;
    -			c1 |= (c2 >> 6) & 0x03;
    -			rs.append(base64_code[c1 & 0x3f]);
    -			rs.append(base64_code[c2 & 0x3f]);
    -		}
    -		return rs.toString();
    -	}
    -
    -	/**
    -	 * Look up the 3 bits base64-encoded by the specified character,
    -	 * range-checking againt conversion table
    -	 * @param x	the base64-encoded value
    -	 * @return	the decoded value of x
    -	 */
    -	private static byte char64(char x) {
    -		if ((int)x < 0 || (int)x > index_64.length)
    -			return -1;
    -		return index_64[(int)x];
    -	}
    -
    -	/**
    -	 * Decode a string encoded using bcrypt's base64 scheme to a
    -	 * byte array. Note that this is *not* compatible with
    -	 * the standard MIME-base64 encoding.
    -	 * @param s	the string to decode
    -	 * @param maxolen	the maximum number of bytes to decode
    -	 * @return	an array containing the decoded bytes
    -	 * @throws IllegalArgumentException if maxolen is invalid
    -	 */
    -	private static byte[] decode_base64(String s, int maxolen)
    -		throws IllegalArgumentException {
    -		StringBuffer rs = new StringBuffer();
    -		int off = 0, slen = s.length(), olen = 0;
    -		byte ret[];
    -		byte c1, c2, c3, c4, o;
    -
    -		if (maxolen <= 0)
    -			throw new IllegalArgumentException ("Invalid maxolen");
    -
    -		while (off < slen - 1 && olen < maxolen) {
    -			c1 = char64(s.charAt(off++));
    -			c2 = char64(s.charAt(off++));
    -			if (c1 == -1 || c2 == -1)
    -				break;
    -			o = (byte)(c1 << 2);
    -			o |= (c2 & 0x30) >> 4;
    -			rs.append((char)o);
    -			if (++olen >= maxolen || off >= slen)
    -				break;
    -			c3 = char64(s.charAt(off++));
    -			if (c3 == -1)
    -				break;
    -			o = (byte)((c2 & 0x0f) << 4);
    -			o |= (c3 & 0x3c) >> 2;
    -			rs.append((char)o);
    -			if (++olen >= maxolen || off >= slen)
    -				break;
    -			c4 = char64(s.charAt(off++));
    -			o = (byte)((c3 & 0x03) << 6);
    -			o |= c4;
    -			rs.append((char)o);
    -			++olen;
    -		}
    -
    -		ret = new byte[olen];
    -		for (off = 0; off < olen; off++)
    -			ret[off] = (byte)rs.charAt(off);
    -		return ret;
    -	}
    -
    -	/**
    -	 * Blowfish encipher a single 64-bit block encoded as
    -	 * two 32-bit halves
    -	 * @param lr	an array containing the two 32-bit half blocks
    -	 * @param off	the position in the array of the blocks
    -	 */
    -	private final void encipher(int lr[], int off) {
    -		int i, n, l = lr[off], r = lr[off + 1];
    -
    -		l ^= P[0];
    -		for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) {
    -			// Feistel substitution on left word
    -			n = S[(l >> 24) & 0xff];
    -			n += S[0x100 | ((l >> 16) & 0xff)];
    -			n ^= S[0x200 | ((l >> 8) & 0xff)];
    -			n += S[0x300 | (l & 0xff)];
    -			r ^= n ^ P[++i];
    -
    -			// Feistel substitution on right word
    -			n = S[(r >> 24) & 0xff];
    -			n += S[0x100 | ((r >> 16) & 0xff)];
    -			n ^= S[0x200 | ((r >> 8) & 0xff)];
    -			n += S[0x300 | (r & 0xff)];
    -			l ^= n ^ P[++i];
    -		}
    -		lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1];
    -		lr[off + 1] = l;
    -	}
    -
    -	/**
    -	 * Cycically extract a word of key material
    -	 * @param data	the string to extract the data from
    -	 * @param offp	a "pointer" (as a one-entry array) to the
    -	 * current offset into data
    -	 * @return	the next word of material from data
    -	 */
    -	private static int streamtoword(byte data[], int offp[]) {
    -		int i;
    -		int word = 0;
    -		int off = offp[0];
    -
    -		for (i = 0; i < 4; i++) {
    -			word = (word << 8) | (data[off] & 0xff);
    -			off = (off + 1) % data.length;
    -		}
    -
    -		offp[0] = off;
    -		return word;
    -	}
    -
    -	/**
    -	 * Initialise the Blowfish key schedule
    -	 */
    -	private void init_key() {
    -		P = (int[])P_orig.clone();
    -		S = (int[])S_orig.clone();
    -	}
    -
    -	/**
    -	 * Key the Blowfish cipher
    -	 * @param key	an array containing the key
    -	 */
    -	private void key(byte key[]) {
    -		int i;
    -		int koffp[] = { 0 };
    -		int lr[] = { 0, 0 };
    -		int plen = P.length, slen = S.length;
    -
    -		for (i = 0; i < plen; i++)
    -			P[i] = P[i] ^ streamtoword(key, koffp);
    -
    -		for (i = 0; i < plen; i += 2) {
    -			encipher(lr, 0);
    -			P[i] = lr[0];
    -			P[i + 1] = lr[1];
    -		}
    -
    -		for (i = 0; i < slen; i += 2) {
    -			encipher(lr, 0);
    -			S[i] = lr[0];
    -			S[i + 1] = lr[1];
    -		}
    -	}
    -
    -	/**
    -	 * Perform the "enhanced key schedule" step described by
    -	 * Provos and Mazieres in "A Future-Adaptable Password Scheme"
    -	 * http://www.openbsd.org/papers/bcrypt-paper.ps
    -	 * @param data	salt information
    -	 * @param key	password information
    -	 */
    -	private void ekskey(byte data[], byte key[]) {
    -		int i;
    -		int koffp[] = { 0 }, doffp[] = { 0 };
    -		int lr[] = { 0, 0 };
    -		int plen = P.length, slen = S.length;
    -
    -		for (i = 0; i < plen; i++)
    -			P[i] = P[i] ^ streamtoword(key, koffp);
    -
    -		for (i = 0; i < plen; i += 2) {
    -			lr[0] ^= streamtoword(data, doffp);
    -			lr[1] ^= streamtoword(data, doffp);
    -			encipher(lr, 0);
    -			P[i] = lr[0];
    -			P[i + 1] = lr[1];
    -		}
    -
    -		for (i = 0; i < slen; i += 2) {
    -			lr[0] ^= streamtoword(data, doffp);
    -			lr[1] ^= streamtoword(data, doffp);
    -			encipher(lr, 0);
    -			S[i] = lr[0];
    -			S[i + 1] = lr[1];
    -		}
    -	}
    -
    -	/**
    -	 * Perform the central password hashing step in the
    -	 * bcrypt scheme
    -	 * @param password	the password to hash
    -	 * @param salt	the binary salt to hash with the password
    -	 * @param log_rounds	the binary logarithm of the number
    -	 * of rounds of hashing to apply
    -	 * @param cdata         the plaintext to encrypt
    -	 * @return	an array containing the binary hashed password
    -	 */
    -	public byte[] crypt_raw(byte password[], byte salt[], int log_rounds,
    -	    int cdata[]) {
    -		int rounds, i, j;
    -		int clen = cdata.length;
    -		byte ret[];
    -
    -		if (log_rounds < 4 || log_rounds > 30)
    -			throw new IllegalArgumentException ("Bad number of rounds");
    -		rounds = 1 << log_rounds;
    -		if (salt.length != BCRYPT_SALT_LEN)
    -			throw new IllegalArgumentException ("Bad salt length");
    -
    -		init_key();
    -		ekskey(salt, password);
    -		for (i = 0; i != rounds; i++) {
    -			key(password);
    -			key(salt);
    -		}
    -
    -		for (i = 0; i < 64; i++) {
    -			for (j = 0; j < (clen >> 1); j++)
    -				encipher(cdata, j << 1);
    -		}
    -
    -		ret = new byte[clen * 4];
    -		for (i = 0, j = 0; i < clen; i++) {
    -			ret[j++] = (byte)((cdata[i] >> 24) & 0xff);
    -			ret[j++] = (byte)((cdata[i] >> 16) & 0xff);
    -			ret[j++] = (byte)((cdata[i] >> 8) & 0xff);
    -			ret[j++] = (byte)(cdata[i] & 0xff);
    -		}
    -		return ret;
    -	}
    -
    -	/**
    -	 * Hash a password using the OpenBSD bcrypt scheme
    -	 * @param password	the password to hash
    -	 * @param salt	the salt to hash with (perhaps generated
    -	 * using BCrypt.gensalt)
    -	 * @return	the hashed password
    -	 */
    -	public static String hashpw(String password, String salt) {
    -		BCrypt B;
    -		String real_salt;
    -		byte passwordb[], saltb[], hashed[];
    -		char minor = (char)0;
    -		int rounds, off = 0;
    -		StringBuffer rs = new StringBuffer();
    -
    -		if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
    -			throw new IllegalArgumentException ("Invalid salt version");
    -		if (salt.charAt(2) == '$')
    -			off = 3;
    -		else {
    -			minor = salt.charAt(2);
    -			if (minor != 'a' || salt.charAt(3) != '$')
    -				throw new IllegalArgumentException ("Invalid salt revision");
    -			off = 4;
    -		}
    -
    -		// Extract number of rounds
    -		if (salt.charAt(off + 2) > '$')
    -			throw new IllegalArgumentException ("Missing salt rounds");
    -		rounds = Integer.parseInt(salt.substring(off, off + 2));
    -
    -		real_salt = salt.substring(off + 3, off + 25);
    -		try {
    -			passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
    -		} catch (UnsupportedEncodingException uee) {
    -			throw new AssertionError("UTF-8 is not supported");
    -		}
    -
    -		saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
    -
    -		B = new BCrypt();
    -		hashed = B.crypt_raw(passwordb, saltb, rounds,
    -		    (int[])bf_crypt_ciphertext.clone());
    -
    -		rs.append("$2");
    -		if (minor >= 'a')
    -			rs.append(minor);
    -		rs.append("$");
    -		if (rounds < 10)
    -			rs.append("0");
    -		if (rounds > 30) {
    -			throw new IllegalArgumentException(
    -			    "rounds exceeds maximum (30)");
    -		}
    -		rs.append(Integer.toString(rounds));
    -		rs.append("$");
    -		rs.append(encode_base64(saltb, saltb.length));
    -		rs.append(encode_base64(hashed,
    -		    bf_crypt_ciphertext.length * 4 - 1));
    -		return rs.toString();
    -	}
    -
    -	/**
    -	 * Generate a salt for use with the BCrypt.hashpw() method
    -	 * @param log_rounds	the log2 of the number of rounds of
    -	 * hashing to apply - the work factor therefore increases as
    -	 * 2**log_rounds.
    -	 * @param random		an instance of SecureRandom to use
    -	 * @return	an encoded salt value
    -	 */
    -	public static String gensalt(int log_rounds, SecureRandom random) {
    -		StringBuffer rs = new StringBuffer();
    -		byte rnd[] = new byte[BCRYPT_SALT_LEN];
    -
    -		random.nextBytes(rnd);
    -
    -		rs.append("$2a$");
    -		if (log_rounds < 10)
    -			rs.append("0");
    -		if (log_rounds > 30) {
    -			throw new IllegalArgumentException(
    -			    "log_rounds exceeds maximum (30)");
    -		}
    -		rs.append(Integer.toString(log_rounds));
    -		rs.append("$");
    -		rs.append(encode_base64(rnd, rnd.length));
    -		return rs.toString();
    -	}
    -
    -	/**
    -	 * Generate a salt for use with the BCrypt.hashpw() method
    -	 * @param log_rounds	the log2 of the number of rounds of
    -	 * hashing to apply - the work factor therefore increases as
    -	 * 2**log_rounds.
    -	 * @return	an encoded salt value
    -	 */
    -	public static String gensalt(int log_rounds) {
    -		return gensalt(log_rounds, new SecureRandom());
    -	}
    -
    -	/**
    -	 * Generate a salt for use with the BCrypt.hashpw() method,
    -	 * selecting a reasonable default for the number of hashing
    -	 * rounds to apply
    -	 * @return	an encoded salt value
    -	 */
    -	public static String gensalt() {
    -		return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
    -	}
    -
    -	/**
    -	 * Check that a plaintext password matches a previously hashed
    -	 * one
    -	 * @param plaintext	the plaintext password to verify
    -	 * @param hashed	the previously-hashed password
    -	 * @return	true if the passwords match, false otherwise
    -	 */
    -	public static boolean checkpw(String plaintext, String hashed) {
    -		byte hashed_bytes[];
    -		byte try_bytes[];
    -		try {
    -			String try_pw = hashpw(plaintext, hashed);
    -			hashed_bytes = hashed.getBytes("UTF-8");
    -			try_bytes = try_pw.getBytes("UTF-8");
    -		} catch (UnsupportedEncodingException uee) {
    -			return false;
    -		}
    -		if (hashed_bytes.length != try_bytes.length)
    -			return false;
    -		byte ret = 0;
    -		for (int i = 0; i < try_bytes.length; i++)
    -			ret |= hashed_bytes[i] ^ try_bytes[i];
    -		return ret == 0;
    -	}
    -}
    diff --git a/core/src/main/java/hudson/security/BasicAuthenticationFilter.java b/core/src/main/java/hudson/security/BasicAuthenticationFilter.java
    index 11709bcf58f1be4f1b311758dd8e2b1eddc3e057..737108560bf007e6140edf977e430c431bd1ddfe 100644
    --- a/core/src/main/java/hudson/security/BasicAuthenticationFilter.java
    +++ b/core/src/main/java/hudson/security/BasicAuthenticationFilter.java
    @@ -27,7 +27,11 @@ import hudson.model.User;
     import jenkins.model.Jenkins;
     import hudson.util.Scrambler;
     import jenkins.security.ApiTokenProperty;
    +import jenkins.security.SecurityListener;
    +import org.acegisecurity.Authentication;
    +import jenkins.security.BasicApiTokenHelper;
     import org.acegisecurity.context.SecurityContextHolder;
    +import org.acegisecurity.userdetails.UserDetails;
     
     import javax.servlet.Filter;
     import javax.servlet.FilterChain;
    @@ -54,14 +58,14 @@ import java.net.URLEncoder;
      *
      * <p>
      * When an HTTP request arrives with an HTTP basic auth header, this filter detects
    - * that and emulate an invocation of <tt>/j_security_check</tt>
    + * that and emulate an invocation of {@code /j_security_check}
      * (see <a href="http://mail-archives.apache.org/mod_mbox/tomcat-users/200105.mbox/%3C9005C0C9C85BD31181B20060085DAC8B10C8EF@tuvi.andmevara.ee%3E">this page</a> for the original technique.)
      *
      * <p>
      * This causes the container to perform authentication, but there's no way
      * to find out whether the user has been successfully authenticated or not.
      * So to find this out, we then redirect the user to
    - * {@link jenkins.model.Jenkins#doSecured(StaplerRequest, StaplerResponse) <tt>/secured/...</tt> page}.
    + * {@link jenkins.model.Jenkins#doSecured(StaplerRequest, StaplerResponse) {@code /secured/...} page}.
      *
      * <p>
      * The handler of the above URL checks if the user is authenticated,
    @@ -75,7 +79,7 @@ import java.net.URLEncoder;
      * <h2>Notes</h2>
      * <ul>
      * <li>
    - * The technique of getting a request dispatcher for <tt>/j_security_check</tt> may not
    + * The technique of getting a request dispatcher for {@code /j_security_check} may not
      * work for all containers, but so far that seems like the only way to make this work.
      * <li>
      * This A → B → A redirect is a cyclic redirection, so we need to watch out for clients
    @@ -132,13 +136,15 @@ public class BasicAuthenticationFilter implements Filter {
                 return;
             }
     
    -        {// attempt to authenticate as API token
    -            // create is true as the user may not have been saved and the default api token may be in use.
    -            // validation of the user will be performed against the underlying realm in impersonate.
    -            User u = User.getById(username, true);
    -            ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
    -            if (t!=null && t.matchesPassword(password)) {
    -                SecurityContextHolder.getContext().setAuthentication(u.impersonate());
    +        {
    +            User u = BasicApiTokenHelper.isConnectingUsingApiToken(username, password);
    +            if(u != null){
    +                UserDetails userDetails = u.getUserDetailsForImpersonation();
    +                Authentication auth = u.impersonate(userDetails);
    +
    +                SecurityListener.fireAuthenticated(userDetails);
    +
    +                SecurityContextHolder.getContext().setAuthentication(auth);
                     try {
                         chain.doFilter(request,response);
                     } finally {
    diff --git a/core/src/main/java/hudson/security/HttpSessionContextIntegrationFilter2.java b/core/src/main/java/hudson/security/HttpSessionContextIntegrationFilter2.java
    index e197c872bf769a9b34d4658a418b359f8d9512ed..7a67c77dd0dadcb133516ea9bad51b0134b34ea6 100644
    --- a/core/src/main/java/hudson/security/HttpSessionContextIntegrationFilter2.java
    +++ b/core/src/main/java/hudson/security/HttpSessionContextIntegrationFilter2.java
    @@ -23,10 +23,13 @@
      */
     package hudson.security;
     
    +import hudson.model.User;
     import jenkins.security.NonSerializableSecurityContext;
    +import jenkins.security.seed.UserSeedProperty;
     import org.acegisecurity.context.HttpSessionContextIntegrationFilter;
     import org.acegisecurity.context.SecurityContext;
     import org.acegisecurity.Authentication;
    +import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
     
     import javax.servlet.ServletException;
     import javax.servlet.ServletRequest;
    @@ -49,16 +52,13 @@ public class HttpSessionContextIntegrationFilter2 extends HttpSessionContextInte
     
         public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
             HttpSession session = ((HttpServletRequest) req).getSession(false);
    -        if(session!=null) {
    -            SecurityContext o = (SecurityContext)session.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);
    -            if(o!=null) {
    +        if (session != null) {
    +            SecurityContext o = (SecurityContext) session.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);
    +            if (o != null) {
                     Authentication a = o.getAuthentication();
    -                if(a!=null) {
    -                    if (a.getPrincipal() instanceof InvalidatableUserDetails) {
    -                        InvalidatableUserDetails ud = (InvalidatableUserDetails) a.getPrincipal();
    -                        if(ud.isInvalid())
    -                            // don't let Acegi see invalid security context
    -                            session.setAttribute(ACEGI_SECURITY_CONTEXT_KEY,null);
    +                if (a != null) {
    +                    if (isAuthInvalidated(a) || hasInvalidSessionSeed(a, session)) {
    +                        session.setAttribute(ACEGI_SECURITY_CONTEXT_KEY, null);
                         }
                     }
                 }
    @@ -66,4 +66,50 @@ public class HttpSessionContextIntegrationFilter2 extends HttpSessionContextInte
     
             super.doFilter(req, res, chain);
         }
    +
    +    private boolean isAuthInvalidated(Authentication authentication) {
    +        if (authentication.getPrincipal() instanceof InvalidatableUserDetails) {
    +            InvalidatableUserDetails ud = (InvalidatableUserDetails) authentication.getPrincipal();
    +            if (ud.isInvalid()) {
    +                // don't let Acegi see invalid security context
    +                return true;
    +            }
    +        }
    +
    +        return false;
    +    }
    +
    +    private boolean hasInvalidSessionSeed(Authentication authentication, HttpSession session) {
    +        if (UserSeedProperty.DISABLE_USER_SEED || authentication instanceof AnonymousAuthenticationToken) {
    +            return false;
    +        }
    +
    +        User userFromSession = User.getById(authentication.getName(), false);
    +        if (userFromSession == null) {
    +            // no requirement for further test as there is no user inside
    +            return false;
    +        }
    +
    +        // for case like recovering backup or other corner cases when the session was not populated by this version
    +        Object userSessionSeedObject = session.getAttribute(UserSeedProperty.USER_SESSION_SEED);
    +        String actualUserSessionSeed;
    +        if (userSessionSeedObject instanceof String) {
    +            actualUserSessionSeed = (String) userSessionSeedObject;
    +        } else {
    +            // the seed must be present AND be a string in the session
    +            return true;
    +        }
    +
    +        UserSeedProperty userSeedProperty = userFromSession.getProperty(UserSeedProperty.class);
    +        if (userSeedProperty == null) {
    +            // if you want to filter out the user seed property, you should consider using the DISABLE_USER_SEED instead
    +            return true;
    +        }
    +        // no need to do a time-constant test here because all the information come from the server
    +        // in other words, there is no way for a user to brute-force those values
    +        boolean validSeed = actualUserSessionSeed.equals(userSeedProperty.getSeed());
    +
    +        // if the authentication is no longer valid we need to remove it from the session
    +        return !validSeed;
    +    }
     }
    diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
    index deaab1ed36a41d4b60b1e034fdda5c06723592bc..c31116113dbb193985bb9db22b5fffacb4cc112d 100644
    --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
    +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
    @@ -24,6 +24,7 @@
     package hudson.security;
     
     import com.thoughtworks.xstream.converters.UnmarshallingContext;
    +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     import hudson.Extension;
     import hudson.ExtensionList;
     import hudson.Util;
    @@ -41,6 +42,9 @@ import hudson.util.PluginServletFilter;
     import hudson.util.Protector;
     import hudson.util.Scrambler;
     import hudson.util.XStream2;
    +import jenkins.security.SecurityListener;
    +import jenkins.util.SystemProperties;
    +import jenkins.security.seed.UserSeedProperty;
     import net.sf.json.JSONObject;
     import org.acegisecurity.Authentication;
     import org.acegisecurity.AuthenticationException;
    @@ -61,8 +65,10 @@ import org.kohsuke.stapler.Stapler;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.interceptor.RequirePOST;
    +import org.mindrot.jbcrypt.BCrypt;
     import org.springframework.dao.DataAccessException;
     
    +import javax.annotation.Nonnull;
     import javax.servlet.Filter;
     import javax.servlet.FilterChain;
     import javax.servlet.FilterConfig;
    @@ -71,16 +77,17 @@ import javax.servlet.ServletRequest;
     import javax.servlet.ServletResponse;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
    +import javax.servlet.http.HttpSession;
    +
     import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
     import java.io.IOException;
     import java.lang.reflect.Constructor;
     import java.security.SecureRandom;
    -import java.util.ArrayList;
    -import java.util.Collections;
    -import java.util.List;
    -import java.util.MissingResourceException;
    -import java.util.ResourceBundle;
    +import java.util.*;
     import java.util.logging.Logger;
    +import java.util.regex.Matcher;
    +import java.util.regex.Pattern;
    +
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     
    @@ -94,6 +101,15 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
      * @author Kohsuke Kawaguchi
      */
     public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRealm implements ModelObject, AccessControlled {
    +    private static /* not final */ String ID_REGEX = System.getProperty(HudsonPrivateSecurityRealm.class.getName() + ".ID_REGEX");
    +    
    +    /**
    +     * Default REGEX for the user ID check in case the ID_REGEX is not set
    +     * It allows A-Za-z0-9 + "_-"
    +     * in Java {@code \w} is equivalent to {@code [A-Za-z0-9_]} (take care of "_")
    +     */
    +    private static final String DEFAULT_ID_REGEX = "^[\\w-]+$";
    +    
         /**
          * If true, sign up is not allowed.
          * <p>
    @@ -252,11 +268,20 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
          */
         @SuppressWarnings("ACL.impersonate")
         private void loginAndTakeBack(StaplerRequest req, StaplerResponse rsp, User u) throws ServletException, IOException {
    +        HttpSession session = req.getSession(false);
    +        if (session != null) {
    +            // avoid session fixation
    +            session.invalidate();
    +        }
    +        req.getSession(true);
    +        
             // ... and let him login
             Authentication a = new UsernamePasswordAuthenticationToken(u.getId(),req.getParameter("password1"));
             a = this.getSecurityComponents().manager.authenticate(a);
             SecurityContextHolder.getContext().setAuthentication(a);
     
    +        SecurityListener.fireLoggedIn(u.getId());
    +
             // then back to top
             req.getView(this,"success.jelly").forward(req,rsp);
         }
    @@ -285,6 +310,35 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
             return u;
         }
     
    +    /**
    +     * Creates a user account. Intended to be called from the setup wizard.
    +     * Note that this method does not check whether it is actually called from
    +     * the setup wizard. This requires the {@link Jenkins#ADMINISTER} permission.
    +     *
    +     * @param req the request to retrieve input data from
    +     * @return the created user account, never null
    +     * @throws AccountCreationFailedException if account creation failed due to invalid form input
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public User createAccountFromSetupWizard(StaplerRequest req) throws IOException, AccountCreationFailedException {
    +        checkPermission(Jenkins.ADMINISTER);
    +        SignupInfo si = validateAccountCreationForm(req, false);
    +        if (!si.errors.isEmpty()) {
    +            String messages = getErrorMessages(si);
    +            throw new AccountCreationFailedException(messages);
    +        } else {
    +            return createAccount(si);
    +        }
    +    }
    +
    +    private String getErrorMessages(SignupInfo si) {
    +        StringBuilder messages = new StringBuilder();
    +        for (String message : si.errors.values()) {
    +            messages.append(message).append(" | ");
    +        }
    +        return messages.toString();
    +    }
    +
         /**
          * Creates a first admin user account.
          *
    @@ -317,65 +371,109 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
         }
     
         /**
    +     * @param req the request to get the form data from (is also used for redirection)
    +     * @param rsp the response to use for forwarding if the creation fails
    +     * @param validateCaptcha whether to attempt to validate a captcha in the request
    +     * @param formView the view to redirect to if creation fails
    +     *
          * @return
          *      null if failed. The browser is already redirected to retry by the time this method returns.
          *      a valid {@link User} object if the user creation was successful.
          */
    -    private User createAccount(StaplerRequest req, StaplerResponse rsp, boolean selfRegistration, String formView) throws ServletException, IOException {
    +    private User createAccount(StaplerRequest req, StaplerResponse rsp, boolean validateCaptcha, String formView) throws ServletException, IOException {
    +        SignupInfo si = validateAccountCreationForm(req, validateCaptcha);
    +
    +        if (!si.errors.isEmpty()) {
    +            // failed. ask the user to try again.
    +            req.getView(this, formView).forward(req, rsp);
    +            return null;
    +        }
    +
    +        return createAccount(si);
    +    }
    +
    +    /**
    +     * @param req              the request to process
    +     * @param validateCaptcha  whether to attempt to validate a captcha in the request
    +     *
    +     * @return a {@link SignupInfo#SignupInfo(StaplerRequest) SignupInfo from given request}, with {@link
    +     * SignupInfo#errors} containing errors (keyed by field name), if any of the supported fields are invalid
    +     */
    +    private SignupInfo validateAccountCreationForm(StaplerRequest req, boolean validateCaptcha) {
             // form field validation
             // this pattern needs to be generalized and moved to stapler
             SignupInfo si = new SignupInfo(req);
     
    -        if(selfRegistration && !validateCaptcha(si.captcha))
    -            si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_TextNotMatchWordInImage();
    -
    -        if(si.password1 != null && !si.password1.equals(si.password2))
    -            si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_PasswordNotMatch();
    -
    -        if(!(si.password1 != null && si.password1.length() != 0))
    -            si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_PasswordRequired();
    +        if (validateCaptcha && !validateCaptcha(si.captcha)) {
    +            si.errors.put("captcha", Messages.HudsonPrivateSecurityRealm_CreateAccount_TextNotMatchWordInImage());
    +        }
     
    -        if(si.username==null || si.username.length()==0)
    -            si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_UserNameRequired();
    -        else {
    +        if (si.username == null || si.username.length() == 0) {
    +            si.errors.put("username", Messages.HudsonPrivateSecurityRealm_CreateAccount_UserNameRequired());
    +        } else if(!containsOnlyAcceptableCharacters(si.username)) {
    +            if (ID_REGEX == null) {
    +                si.errors.put("username", Messages.HudsonPrivateSecurityRealm_CreateAccount_UserNameInvalidCharacters());
    +            } else {
    +                si.errors.put("username", Messages.HudsonPrivateSecurityRealm_CreateAccount_UserNameInvalidCharactersCustom(ID_REGEX));
    +            }
    +        } else {
                 // do not create the user - we just want to check if the user already exists but is not a "login" user.
    -            User user = User.getById(si.username, false); 
    +            User user = User.getById(si.username, false);
                 if (null != user)
                     // Allow sign up. SCM people has no such property.
                     if (user.getProperty(Details.class) != null)
    -                    si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_UserNameAlreadyTaken();
    +                    si.errors.put("username", Messages.HudsonPrivateSecurityRealm_CreateAccount_UserNameAlreadyTaken());
             }
     
    -        if(si.fullname==null || si.fullname.length()==0)
    -            si.fullname = si.username;
    +        if (si.password1 != null && !si.password1.equals(si.password2)) {
    +            si.errors.put("password1", Messages.HudsonPrivateSecurityRealm_CreateAccount_PasswordNotMatch());
    +        }
     
    -        if(isMailerPluginPresent() && (si.email==null || !si.email.contains("@")))
    -            si.errorMessage = Messages.HudsonPrivateSecurityRealm_CreateAccount_InvalidEmailAddress();
    +        if (!(si.password1 != null && si.password1.length() != 0)) {
    +            si.errors.put("password1", Messages.HudsonPrivateSecurityRealm_CreateAccount_PasswordRequired());
    +        }
     
    -        if (! User.isIdOrFullnameAllowed(si.username)) {
    -            si.errorMessage = hudson.model.Messages.User_IllegalUsername(si.username);
    +        if (si.fullname == null || si.fullname.length() == 0) {
    +            si.fullname = si.username;
             }
     
    -        if (! User.isIdOrFullnameAllowed(si.fullname)) {
    -            si.errorMessage = hudson.model.Messages.User_IllegalFullname(si.fullname);
    +        if (isMailerPluginPresent() && (si.email == null || !si.email.contains("@"))) {
    +            si.errors.put("email", Messages.HudsonPrivateSecurityRealm_CreateAccount_InvalidEmailAddress());
             }
     
    -        if(si.errorMessage!=null) {
    -            // failed. ask the user to try again.
    -            req.setAttribute("data",si);
    -            req.getView(this, formView).forward(req,rsp);
    -            return null;
    +        if (!User.isIdOrFullnameAllowed(si.username)) {
    +            si.errors.put("username", hudson.model.Messages.User_IllegalUsername(si.username));
             }
     
    +        if (!User.isIdOrFullnameAllowed(si.fullname)) {
    +            si.errors.put("fullname", hudson.model.Messages.User_IllegalFullname(si.fullname));
    +        }
    +        req.setAttribute("data", si); // for error messages in the view
    +        return si;
    +    }
    +
    +    /**
    +     * Creates a new account from a valid signup info. A signup info is valid if its {@link SignupInfo#errors}
    +     * field is empty.
    +     *
    +     * @param si the valid signup info to create an account from
    +     * @return a valid {@link User} object created from given signup info
    +     * @throws IllegalArgumentException if an invalid signup info is passed
    +     */
    +    private User createAccount(SignupInfo si) throws IOException {
    +        if (!si.errors.isEmpty()) {
    +            String messages = getErrorMessages(si);
    +            throw new IllegalArgumentException("invalid signup info passed to createAccount(si): " + messages);
    +        }
             // register the user
    -        User user = createAccount(si.username,si.password1);
    +        User user = createAccount(si.username, si.password1);
             user.setFullName(si.fullname);
    -        if(isMailerPluginPresent()) {
    +        if (isMailerPluginPresent()) {
                 try {
                     // legacy hack. mail support has moved out to a separate plugin
                     Class<?> up = Jenkins.getInstance().pluginManager.uberClassLoader.loadClass("hudson.tasks.Mailer$UserProperty");
                     Constructor<?> c = up.getDeclaredConstructor(String.class);
    -                user.addProperty((UserProperty)c.newInstance(si.email));
    +                user.addProperty((UserProperty) c.newInstance(si.email));
                 } catch (ReflectiveOperationException e) {
                     throw new RuntimeException(e);
                 }
    @@ -383,7 +481,15 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
             user.save();
             return user;
         }
    -    
    +
    +    private boolean containsOnlyAcceptableCharacters(@Nonnull String value){
    +        if(ID_REGEX == null){
    +            return value.matches(DEFAULT_ID_REGEX);
    +        }else{
    +            return value.matches(ID_REGEX);
    +        }
    +    }
    +
         @Restricted(NoExternalUse.class)
         public boolean isMailerPluginPresent() {
             try {
    @@ -401,9 +507,27 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
         public User createAccount(String userName, String password) throws IOException {
             User user = User.getById(userName, true);
             user.addProperty(Details.fromPlainPassword(password));
    +        SecurityListener.fireUserCreated(user.getId());
             return user;
         }
     
    +    /**
    +     * Creates a new user account by registering a JBCrypt Hashed password with the user.
    +     *
    +     * @param userName The user's name
    +     * @param hashedPassword A hashed password, must begin with <code>#jbcrypt:</code>
    +     */
    +    public User createAccountWithHashedPassword(String userName, String hashedPassword) throws IOException {
    +        if (!PASSWORD_ENCODER.isPasswordHashed(hashedPassword)) {
    +            throw new IllegalArgumentException("this method should only be called with a pre-hashed password");
    +        }
    +        User user = User.getById(userName, true);
    +        user.addProperty(Details.fromHashedPassword(hashedPassword));
    +        SecurityListener.fireUserCreated(user.getId());
    +        return user;
    +    }
    +
    +
         /**
          * This is used primarily when the object is listed in the breadcrumb, in the user management screen.
          */
    @@ -441,8 +565,9 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
          * This is to map users under the security realm URL.
          * This in turn helps us set up the right navigation breadcrumb.
          */
    +    @Restricted(NoExternalUse.class)
         public User getUser(String id) {
    -        return User.getById(id, true);
    +        return User.getById(id, User.ALLOW_USER_CREATION_VIA_URL && hasPermission(Jenkins.ADMINISTER));
         }
     
         // TODO
    @@ -452,10 +577,18 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
             public String username,password1,password2,fullname,email,captcha;
     
             /**
    -         * To display an error message, set it here.
    +         * To display a general error message, set it here.
    +         *
              */
             public String errorMessage;
     
    +        /**
    +         * Add field-specific error messages here.
    +         * Keys are field names (e.g. {@code password2}), values are the messages.
    +         */
    +        // TODO i18n?
    +        public HashMap<String, String> errors = new HashMap<String, String>();
    +
             public SignupInfo() {
             }
     
    @@ -590,6 +723,14 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
                         if(data.startsWith(prefix))
                             return Details.fromHashedPassword(data.substring(prefix.length()));
                     }
    +
    +                User user = Util.getNearestAncestorOfTypeOrThrow(req, User.class);
    +                // the UserSeedProperty is not touched by the configure page
    +                UserSeedProperty userSeedProperty = user.getProperty(UserSeedProperty.class);
    +                if (userSeedProperty != null) {
    +                    userSeedProperty.renewSeed();
    +                }
    +
                     return Details.fromPlainPassword(Util.fixNull(pwd));
                 }
     
    @@ -637,7 +778,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
          *
          * <p>
          * The salt is prepended to the hashed password and returned. So the encoded password is of the form
    -     * <tt>SALT ':' hash(PASSWORD,SALT)</tt>.
    +     * {@code SALT ':' hash(PASSWORD,SALT)}.
          *
          * <p>
          * This abbreviates the need to store the salt separately, which in turn allows us to hide the salt handling
    @@ -646,11 +787,11 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
         /*package*/ static final PasswordEncoder CLASSIC = new PasswordEncoder() {
             private final PasswordEncoder passwordEncoder = new ShaPasswordEncoder(256);
     
    -        public String encodePassword(String rawPass, Object _) throws DataAccessException {
    +        public String encodePassword(String rawPass, Object obj) throws DataAccessException {
                 return hash(rawPass);
             }
     
    -        public boolean isPasswordValid(String encPass, String rawPass, Object _) throws DataAccessException {
    +        public boolean isPasswordValid(String encPass, String rawPass, Object obj) throws DataAccessException {
                 // pull out the sale from the encoded password
                 int i = encPass.indexOf(':');
                 if(i<0) return false;
    @@ -685,21 +826,54 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
         /**
          * {@link PasswordEncoder} that uses jBCrypt.
          */
    -    private static final PasswordEncoder JBCRYPT_ENCODER = new PasswordEncoder() {
    -        public String encodePassword(String rawPass, Object _) throws DataAccessException {
    +    private static class JBCryptEncoder implements PasswordEncoder {
    +        // in jBCrypt the maximum is 30, which takes ~22h with laptop late-2017
    +        // and for 18, it's "only" 20s
    +        @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
    +        @Restricted(NoExternalUse.class)
    +        private static int MAXIMUM_BCRYPT_LOG_ROUND = SystemProperties.getInteger(HudsonPrivateSecurityRealm.class.getName() + ".maximumBCryptLogRound", 18);
    +
    +        private static final Pattern BCRYPT_PATTERN = Pattern.compile("^\\$2a\\$([0-9]{2})\\$.{53}$");
    +
    +        public String encodePassword(String rawPass, Object obj) throws DataAccessException {
                 return BCrypt.hashpw(rawPass,BCrypt.gensalt());
             }
     
    -        public boolean isPasswordValid(String encPass, String rawPass, Object _) throws DataAccessException {
    +        public boolean isPasswordValid(String encPass, String rawPass, Object obj) throws DataAccessException {
                 return BCrypt.checkpw(rawPass,encPass);
             }
    -    };
    +
    +        /**
    +         * Returns true if the supplied hash looks like a bcrypt encoded hash value, based off of the
    +         * implementation defined in jBCrypt and: https://en.wikipedia.org/wiki/Bcrypt.
    +         *
    +         */
    +        public boolean isHashValid(String hash) {
    +            Matcher matcher = BCRYPT_PATTERN.matcher(hash);
    +            if (matcher.matches()) {
    +                String logNumOfRound = matcher.group(1);
    +                // no number format exception due to the expression
    +                int logNumOfRoundInt = Integer.parseInt(logNumOfRound);
    +                if (logNumOfRoundInt > 0 && logNumOfRoundInt <= MAXIMUM_BCRYPT_LOG_ROUND) {
    +                    return true;
    +                }
    +            }
    +            return false;
    +        }
    +    }
    +
    +    /* package */ static final JBCryptEncoder JBCRYPT_ENCODER = new JBCryptEncoder();
     
         /**
          * Combines {@link #JBCRYPT_ENCODER} and {@link #CLASSIC} into one so that we can continue
          * to accept {@link #CLASSIC} format but new encoding will always done via {@link #JBCRYPT_ENCODER}.
          */
    -    public static final PasswordEncoder PASSWORD_ENCODER = new PasswordEncoder() {
    +    /* package */ static class MultiPasswordEncoder implements PasswordEncoder {
    +        /**
    +         * Magic header used to detect if a password is bcrypt hashed.
    +         */
    +        private static final String JBCRYPT_HEADER = "#jbcrypt:";
    +
             /*
                 CLASSIC encoder outputs "salt:hash" where salt is [a-z]+, so we use unique prefix '#jbcyrpt"
                 to designate JBCRYPT-format hash.
    @@ -711,14 +885,26 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
             }
     
             public boolean isPasswordValid(String encPass, String rawPass, Object salt) throws DataAccessException {
    -            if (encPass.startsWith(JBCRYPT_HEADER))
    -                return JBCRYPT_ENCODER.isPasswordValid(encPass.substring(JBCRYPT_HEADER.length()),rawPass,salt);
    -            else
    -                return CLASSIC.isPasswordValid(encPass,rawPass,salt);
    +            if (isPasswordHashed(encPass)) {
    +                return JBCRYPT_ENCODER.isPasswordValid(encPass.substring(JBCRYPT_HEADER.length()), rawPass, salt);
    +            } else {
    +                return CLASSIC.isPasswordValid(encPass, rawPass, salt);
    +            }
    +        }
    +
    +        /**
    +         * Returns true if the supplied password starts with a prefix indicating it is already hashed.
    +         */
    +        public boolean isPasswordHashed(String password) {
    +            if (password == null) {
    +                return false;
    +            }
    +            return password.startsWith(JBCRYPT_HEADER) && JBCRYPT_ENCODER.isHashValid(password.substring(JBCRYPT_HEADER.length()));
             }
     
    -        private static final String JBCRYPT_HEADER = "#jbcrypt:";
         };
    +    
    +    public static final MultiPasswordEncoder PASSWORD_ENCODER = new MultiPasswordEncoder();
     
         @Extension @Symbol("local")
         public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
    diff --git a/core/src/main/java/hudson/security/LegacySecurityRealm.java b/core/src/main/java/hudson/security/LegacySecurityRealm.java
    index 116e50b2c9e237aaa3bc409ba060f978f01c14c8..8f4cc84e7a72e4bfee3ae06b79a08af1a84437a7 100644
    --- a/core/src/main/java/hudson/security/LegacySecurityRealm.java
    +++ b/core/src/main/java/hudson/security/LegacySecurityRealm.java
    @@ -29,13 +29,12 @@ import org.acegisecurity.AuthenticationException;
     import org.jenkinsci.Symbol;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
    +import org.kohsuke.stapler.DataBoundConstructor;
     import org.springframework.web.context.WebApplicationContext;
    -import org.kohsuke.stapler.StaplerRequest;
     import groovy.lang.Binding;
     import hudson.model.Descriptor;
     import hudson.util.spring.BeanBuilder;
     import hudson.Extension;
    -import net.sf.json.JSONObject;
     
     import javax.servlet.Filter;
     import javax.servlet.FilterConfig;
    @@ -48,6 +47,10 @@ import javax.servlet.FilterConfig;
      * @author Kohsuke Kawaguchi
      */
     public final class LegacySecurityRealm extends SecurityRealm implements AuthenticationManager {
    +    @DataBoundConstructor
    +    public LegacySecurityRealm() {
    +    }
    +
         public SecurityComponents createSecurityComponents() {
             return new SecurityComponents(this);
         }
    @@ -104,10 +107,6 @@ public final class LegacySecurityRealm extends SecurityRealm implements Authenti
                 DESCRIPTOR = this;
             }
     
    -        public SecurityRealm newInstance(StaplerRequest req, JSONObject formData) throws FormException {
    -            return new LegacySecurityRealm();
    -        }
    -
             public String getDisplayName() {
                 return Messages.LegacySecurityRealm_Displayname();
             }
    diff --git a/core/src/main/java/hudson/security/Permission.java b/core/src/main/java/hudson/security/Permission.java
    index 431ad0fd1d887c4579378b67dc60a8b97d0894ea..31f6a944e25f74adea471084798131e473dc218c 100644
    --- a/core/src/main/java/hudson/security/Permission.java
    +++ b/core/src/main/java/hudson/security/Permission.java
    @@ -68,6 +68,9 @@ public final class Permission {
     
         public final @Nonnull PermissionGroup group;
     
    +    // if some plugin serialized old version of this class using XStream, `id` can be null
    +    private final @CheckForNull String id;
    +
         /**
          * Human readable ID of the permission.
          *
    @@ -158,6 +161,7 @@ public final class Permission {
             this.impliedBy = impliedBy;
             this.enabled = enable;
             this.scopes = ImmutableSet.copyOf(scopes);
    +        this.id = owner.getName() + '.' + name;
     
             group.add(this);
             ALL.add(this);
    @@ -222,7 +226,10 @@ public final class Permission {
          * @see #fromId(String)
          */
         public @Nonnull String getId() {
    -        return owner.getName()+'.'+name;
    +        if (id == null) {
    +            return owner.getName() + '.' + name;
    +        }
    +        return id;
         }
     
         @Override public boolean equals(Object o) {
    diff --git a/core/src/main/java/hudson/security/PermissionGroup.java b/core/src/main/java/hudson/security/PermissionGroup.java
    index 13542cf4a884342883700f8e6d945c83fe95dac7..ef39787662f72a53e45c05882bece5ea4f42d032 100644
    --- a/core/src/main/java/hudson/security/PermissionGroup.java
    +++ b/core/src/main/java/hudson/security/PermissionGroup.java
    @@ -27,6 +27,7 @@ import hudson.model.Hudson;
     import java.util.ArrayList;
     import java.util.Iterator;
     import java.util.List;
    +import java.util.Locale;
     import java.util.SortedSet;
     import java.util.TreeSet;
     import javax.annotation.CheckForNull;
    @@ -51,6 +52,8 @@ public final class PermissionGroup implements Iterable<Permission>, Comparable<P
          */
         public final Localizable title;
     
    +    private final String id;
    +
         /**
          * Both creates a registers a new permission group.
          * @param owner sets {@link #owner}
    @@ -58,12 +61,32 @@ public final class PermissionGroup implements Iterable<Permission>, Comparable<P
          * @throws IllegalStateException if this group was already registered
          */
         public PermissionGroup(@Nonnull Class owner, Localizable title) throws IllegalStateException {
    +        this(title.toString(Locale.ENGLISH), owner, title);
    +    }
    +
    +    /**
    +     * Both creates a registers a new permission group.
    +     * @param owner sets {@link #owner}
    +     * @param title sets {@link #title}
    +     * @throws IllegalStateException if this group was already registered
    +     * @since 2.127
    +     */
    +    public PermissionGroup(String id, @Nonnull Class owner, Localizable title) throws IllegalStateException {
             this.owner = owner;
             this.title = title;
    +        this.id = id;
             register(this);
         }
     
    -    private String id() {
    +    /**
    +     * Gets ID of the permission group.
    +     * @return Non-localizable ID of the permission group.
    +     */
    +    public String getId() {
    +        return id;
    +    }
    +
    +    public String getOwnerClassName() {
             return owner.getName();
         }
     
    @@ -110,7 +133,7 @@ public final class PermissionGroup implements Iterable<Permission>, Comparable<P
     
             // among the permissions of the same group, just sort by their names
             // so that the sort order is consistent regardless of classloading order.
    -        return id().compareTo(that.id());
    +        return getOwnerClassName().compareTo(that.getOwnerClassName());
         }
     
         private int compareOrder() {
    @@ -119,11 +142,11 @@ public final class PermissionGroup implements Iterable<Permission>, Comparable<P
         }
     
         @Override public boolean equals(Object o) {
    -        return o instanceof PermissionGroup && id().equals(((PermissionGroup) o).id());
    +        return o instanceof PermissionGroup && getOwnerClassName().equals(((PermissionGroup) o).getOwnerClassName());
         }
     
         @Override public int hashCode() {
    -        return id().hashCode();
    +        return getOwnerClassName().hashCode();
         }
     
         public synchronized int size() {
    @@ -131,12 +154,12 @@ public final class PermissionGroup implements Iterable<Permission>, Comparable<P
         }
     
         @Override public String toString() {
    -        return "PermissionGroup[" + id() + "]";
    +        return "PermissionGroup[" + getOwnerClassName() + "]";
         }
     
         private static synchronized void register(PermissionGroup g) {
             if (!PERMISSIONS.add(g)) {
    -            throw new IllegalStateException("attempt to register a second PermissionGroup for " + g.id());
    +            throw new IllegalStateException("attempt to register a second PermissionGroup for " + g.getOwnerClassName());
             }
         }
     
    diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java
    index 17d24a7aa2a9d58ca176f6f0197eba0b1cfcbe88..9ec5533583a38ca03421a5916c29712d54efe252 100644
    --- a/core/src/main/java/hudson/security/SecurityRealm.java
    +++ b/core/src/main/java/hudson/security/SecurityRealm.java
    @@ -77,7 +77,7 @@ import org.jenkinsci.Symbol;
      *
      * <p>
      * If additional views/URLs need to be exposed,
    - * an active {@link SecurityRealm} is bound to <tt>CONTEXT_ROOT/securityRealm/</tt>
    + * an active {@link SecurityRealm} is bound to {@code CONTEXT_ROOT/securityRealm/}
      * through {@link jenkins.model.Jenkins#getSecurityRealm()}, so you can define additional pages and
      * operations on your {@link SecurityRealm}.
      *
    @@ -145,7 +145,7 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
          * {@link AuthenticationManager} instantiation often depends on the user-specified parameters
          * (for example, if the authentication is based on LDAP, the user needs to specify
          * the host name of the LDAP server.) Such configuration is expected to be
    -     * presented to the user via <tt>config.jelly</tt> and then
    +     * presented to the user via {@code config.jelly} and then
          * captured as instance variables inside the {@link SecurityRealm} implementation.
          *
          * <p>
    @@ -207,8 +207,8 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
          *
          * <p>
          * {@link SecurityRealm} is a singleton resource in Hudson, and therefore
    -     * it's always configured through <tt>config.jelly</tt> and never with
    -     * <tt>global.jelly</tt>. 
    +     * it's always configured through {@code config.jelly} and never with
    +     * {@code global.jelly}.
          */
         @Override
         public Descriptor<SecurityRealm> getDescriptor() {
    @@ -227,7 +227,7 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
          * Gets the target URL of the "login" link.
          * There's no need to override this, except for {@link LegacySecurityRealm}.
          * On legacy implementation this should point to {@code loginEntry}, which
    -     * is protected by <tt>web.xml</tt>, so that the user can be eventually authenticated
    +     * is protected by {@code web.xml}, so that the user can be eventually authenticated
          * by the container.
          *
          * <p>
    @@ -306,6 +306,9 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
     
             // reset remember-me cookie
             Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,"");
    +        cookie.setMaxAge(0);
    +        cookie.setSecure(req.isSecure());
    +        cookie.setHttpOnly(true);
             cookie.setPath(req.getContextPath().length()>0 ? req.getContextPath() : "/");
             rsp.addCookie(cookie);
     
    @@ -314,12 +317,12 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
     
         /**
          * Returns true if this {@link SecurityRealm} allows online sign-up.
    -     * This creates a hyperlink that redirects users to <tt>CONTEXT_ROOT/signUp</tt>,
    -     * which will be served by the <tt>signup.jelly</tt> view of this class.
    +     * This creates a hyperlink that redirects users to {@code CONTEXT_ROOT/signUp},
    +     * which will be served by the {@code signup.jelly} view of this class.
          *
          * <p>
          * If the implementation needs to redirect the user to a different URL
    -     * for signing up, use the following jelly script as <tt>signup.jelly</tt>
    +     * for signing up, use the following jelly script as {@code signup.jelly}
          *
          * <pre>{@code <xmp>
          * <st:redirect url="http://www.sun.com/" xmlns:st="jelly:stapler"/>
    @@ -404,7 +407,10 @@ public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityReal
             if (captchaSupport != null) {
                 String id = req.getSession().getId();
                 rsp.setContentType("image/png");
    -            rsp.addHeader("Cache-Control", "no-cache");
    +            // source: https://stackoverflow.com/a/3414217
    +            rsp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    +            rsp.setHeader("Pragma", "no-cache");
    +            rsp.setHeader("Expires", "0");
                 captchaSupport.generateImage(id, rsp.getOutputStream());
             }
         }
    diff --git a/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java b/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java
    index 9b81ae5b19126ef2c7c31641a880c06ecaf6d4bb..c93d4c77c21f2f98cc3743ea4b128451b6344a91 100644
    --- a/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java
    +++ b/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java
    @@ -24,23 +24,37 @@
     package hudson.security;
     
     import hudson.Functions;
    +import hudson.model.User;
     import jenkins.model.Jenkins;
     import jenkins.security.HMACConfidentialKey;
     import jenkins.security.ImpersonatingUserDetailsService;
     import jenkins.security.LastGrantedAuthoritiesProperty;
    +import jenkins.security.seed.UserSeedProperty;
    +import jenkins.util.SystemProperties;
     import org.acegisecurity.Authentication;
    +import org.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
     import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices;
     import org.acegisecurity.userdetails.UserDetails;
     import org.acegisecurity.userdetails.UserDetailsService;
     import org.apache.commons.codec.binary.Base64;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.springframework.util.Assert;
    +import org.springframework.util.StringUtils;
     
    +import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     import javax.servlet.http.Cookie;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
     import java.lang.reflect.InvocationTargetException;
     import java.lang.reflect.Method;
    +import java.nio.charset.StandardCharsets;
    +import java.security.MessageDigest;
     import java.util.Date;
    +import java.util.concurrent.TimeUnit;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
     
     /**
      * {@link TokenBasedRememberMeServices} with modification so as not to rely
    @@ -53,6 +67,16 @@ import java.util.Date;
      * @author Kohsuke Kawaguchi
      */
     public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices {
    +
    +    private static final Logger LOGGER = Logger.getLogger(TokenBasedRememberMeServices2.class.getName());
    +
    +    /**
    +     * Escape hatch for the check on the maximum date for the expiration duration of the remember me cookie
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static /* Script Console modifiable */ boolean SKIP_TOO_FAR_EXPIRATION_DATE_CHECK = 
    +            SystemProperties.getBoolean(TokenBasedRememberMeServices2.class.getName() + ".skipTooFarExpirationDateCheck");
    +
         /**
          * Decorate {@link UserDetailsService} so that we can use information stored in
          * {@link LastGrantedAuthoritiesProperty}.
    @@ -70,9 +94,23 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices
     
         @Override
         protected String makeTokenSignature(long tokenExpiryTime, UserDetails userDetails) {
    -        String expectedTokenSignature = MAC.mac(userDetails.getUsername() + ":" + tokenExpiryTime + ":"
    -                + "N/A" + ":" + getKey());
    -        return expectedTokenSignature;
    +        String userSeed;
    +        if (UserSeedProperty.DISABLE_USER_SEED) {
    +            userSeed = "no-seed";
    +        } else {
    +            User user = User.getById(userDetails.getUsername(), false);
    +            if (user == null) {
    +                return "no-user";
    +            }
    +            UserSeedProperty userSeedProperty = user.getProperty(UserSeedProperty.class);
    +            if (userSeedProperty == null) {
    +                // if you want to filter out the user seed property, you should consider using the DISABLE_USER_SEED instead
    +                return "no-prop";
    +            }
    +            userSeed = userSeedProperty.getSeed();
    +        }
    +        String token = String.join(":", userDetails.getUsername(), Long.toString(tokenExpiryTime), userSeed, getKey());
    +        return MAC.mac(token);
         }
     
         @Override
    @@ -107,7 +145,7 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices
     		Assert.notNull(successfulAuthentication.getCredentials());
     		Assert.isInstanceOf(UserDetails.class, successfulAuthentication.getPrincipal());
     
    -		long expiryTime = System.currentTimeMillis() + (tokenValiditySeconds * 1000);
    +		long expiryTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(tokenValiditySeconds);
     		String username = ((UserDetails) successfulAuthentication.getPrincipal()).getUsername();
     
     		String signatureValue = makeTokenSignature(expiryTime, (UserDetails)successfulAuthentication.getPrincipal());
    @@ -123,19 +161,154 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices
     
         @Override
         public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    +        if(Jenkins.getInstance().isDisableRememberMe()){
    +            cancelCookie(request, response, null);
    +            return null;
    +        }else {
    +            try {
    +                // we use a patched version of the super.autoLogin
    +                String rememberMeValue = findRememberMeCookieValue(request, response);
    +                if (rememberMeValue == null) {
    +                    return null;
    +                }
    +                return retrieveAuthFromCookie(request, response, rememberMeValue);
    +            } catch (Exception e) {
    +                cancelCookie(request, response, "Failed to handle remember-me cookie: " + Functions.printThrowable(e));
    +                return null;
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Patched version of the super.autoLogin with a time-independent equality check for the token validation
    +     */
    +    private String findRememberMeCookieValue(HttpServletRequest request, HttpServletResponse response) {
    +        Cookie[] cookies = request.getCookies();
    +
    +        if ((cookies == null) || (cookies.length == 0)) {
    +            return null;
    +        }
    +
    +        for (int i = 0; i < cookies.length; i++) {
    +            if (ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY.equals(cookies[i].getName())) {
    +                return cookies[i].getValue();
    +            }
    +        }
    +
    +        return null;
    +    }
    +
    +    // Taken from TokenBasedRememberMeService with slight modification
    +    // around the token equality check to avoid timing attack
    +    // and reducing drastically the information provided in the log to avoid potential disclosure
    +    private @CheckForNull Authentication retrieveAuthFromCookie(HttpServletRequest request, HttpServletResponse response, String cookieValueBase64){
    +        String cookieValue = decodeCookieBase64(cookieValueBase64);
    +        if (cookieValue == null) {
    +            String reason = "Cookie token was not Base64 encoded; value was '" + cookieValueBase64 + "'";
    +            cancelCookie(request, response, reason);
    +            return null;
    +        }
    +        if (logger.isDebugEnabled()) {
    +            logger.debug("Remember-me cookie detected");
    +        }
    +
    +        String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieValue, ":");
    +
    +        if (cookieTokens.length != 3) {
    +            cancelCookie(request, response, "Cookie token did not contain 3 tokens separated by [:]");
    +            return null;
    +        }
    +
    +        long tokenExpiryTime;
    +
    +        try {
    +            tokenExpiryTime = Long.parseLong(cookieTokens[1]);
    +        }
    +        catch (NumberFormatException nfe) {
    +            cancelCookie(request, response, "Cookie token[1] did not contain a valid number");
    +            return null;
    +        }
    +
    +        if (isTokenExpired(tokenExpiryTime)) {
    +            cancelCookie(request, response, "Cookie token[1] has expired");
    +            return null;
    +        }
    +
    +        // Check the user exists
    +        // Defer lookup until after expiry time checked, to
    +        // possibly avoid expensive lookup
    +        UserDetails userDetails = loadUserDetails(request, response, cookieTokens);
    +
    +        if (userDetails == null) {
    +            cancelCookie(request, response, "Cookie token[0] contained a username without user associated");
    +            return null;
    +        }
    +
    +        if (!isValidUserDetails(request, response, userDetails, cookieTokens)) {
    +            return null;
    +        }
    +
    +        String receivedTokenSignature = cookieTokens[2];
    +        String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails);
    +
    +        boolean tokenValid = MessageDigest.isEqual(
    +                expectedTokenSignature.getBytes(StandardCharsets.US_ASCII),
    +                receivedTokenSignature.getBytes(StandardCharsets.US_ASCII)
    +        );
    +        if (!tokenValid) {
    +            cancelCookie(request, response, "Cookie token[2] contained invalid signature");
    +            return null;
    +        }
    +
    +        // By this stage we have a valid token
    +        if (logger.isDebugEnabled()) {
    +            logger.debug("Remember-me cookie accepted");
    +        }
    +
    +        RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(this.getKey(), userDetails,
    +                userDetails.getAuthorities());
    +        auth.setDetails(authenticationDetailsSource.buildDetails(request));
    +
    +        return auth;
    +    }
    +
    +    /**
    +     * @return the decoded base64 of the cookie or {@code null} if the value was not correctly encoded
    +     */
    +    private @CheckForNull String decodeCookieBase64(@Nonnull String base64EncodedValue){
    +        for (int j = 0; j < base64EncodedValue.length() % 4; j++) {
    +            base64EncodedValue = base64EncodedValue + "=";
    +        }
    +
             try {
    -            return super.autoLogin(request, response);
    -        } catch (Exception e) {
    -            cancelCookie(request, response, "Failed to handle remember-me cookie: "+Functions.printThrowable(e));
    +            // any charset should be fine but better safe than sorry
    +            byte[] decodedPlainValue = java.util.Base64.getDecoder().decode(base64EncodedValue.getBytes(StandardCharsets.UTF_8));
    +            return new String(decodedPlainValue, StandardCharsets.UTF_8);
    +        } catch (IllegalArgumentException e) {
                 return null;
             }
         }
     
    -	@Override
    -	protected Cookie makeValidCookie(String tokenValueBase64, HttpServletRequest request, long maxAge) {
    -		Cookie cookie = super.makeValidCookie(tokenValueBase64, request, maxAge);
    +    @Override
    +    protected Cookie makeValidCookie(String tokenValueBase64, HttpServletRequest request, long maxAge) {
    +        Cookie cookie = super.makeValidCookie(tokenValueBase64, request, maxAge);
    +        secureCookie(cookie, request);
    +        return cookie;
    +    }
    +
    +    @Override 
    +    protected Cookie makeCancelCookie(HttpServletRequest request) {
    +        Cookie cookie = super.makeCancelCookie(request);
    +        secureCookie(cookie, request);
    +        return cookie;
    +    }
    +    
    +    /**
    +     * Force always the http-only flag and depending on the request, put also the secure flag.
    +     */
    +    private void secureCookie(Cookie cookie, HttpServletRequest request){
             // if we can mark the cookie HTTP only, do so to protect this cookie even in case of XSS vulnerability.
    -		if (SET_HTTP_ONLY!=null) {
    +        if (SET_HTTP_ONLY!=null) {
                 try {
                     SET_HTTP_ONLY.invoke(cookie,true);
                 } catch (IllegalAccessException e) {
    @@ -148,12 +321,32 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices
             // if the user is running Jenkins over HTTPS, we also want to prevent the cookie from leaking in HTTP.
             // whether the login is done over HTTPS or not would be a good enough approximation of whether Jenkins runs in
             // HTTPS or not, so use that.
    -        if (request.isSecure())
    -            cookie.setSecure(true);
    -		return cookie;
    -	}
    +        cookie.setSecure(request.isSecure());
    +    }
     
    -	/**
    +    /**
    +     * In addition to the expiration requested by the super class, we also check the expiration is not too far in the future.
    +     * Especially to detect maliciously crafted cookie.
    +     */
    +    @Override
    +    protected boolean isTokenExpired(long tokenExpiryTimeMs) {
    +        long nowMs = System.currentTimeMillis();
    +        long maxExpirationMs = TimeUnit.SECONDS.toMillis(tokenValiditySeconds) + nowMs;
    +        if(!SKIP_TOO_FAR_EXPIRATION_DATE_CHECK && tokenExpiryTimeMs > maxExpirationMs){
    +            // attempt to use a cookie that has more than the maximum allowed expiration duration
    +            // was either created before a change of configuration or maliciously crafted
    +            long diffMs = tokenExpiryTimeMs - maxExpirationMs;
    +            LOGGER.log(Level.WARNING, "Attempt to use a cookie with an expiration duration larger than the one configured (delta of: {0} ms)", diffMs);
    +            return true;
    +        }
    +        // Check it has not expired
    +        if (tokenExpiryTimeMs < nowMs) {
    +            return true;
    +        }
    +        return false;
    +    }
    +
    +    /**
          * Used to compute the token signature securely.
          */
         private static final HMACConfidentialKey MAC = new HMACConfidentialKey(TokenBasedRememberMeServices.class,"mac");
    diff --git a/core/src/main/java/hudson/security/WhoAmI.java b/core/src/main/java/hudson/security/WhoAmI.java
    index 76faab1948af8148f6b48b3dad262a750b5cbdb3..66e837acd37ffe2fa5ab1f84de0837b15bbae2b4 100644
    --- a/core/src/main/java/hudson/security/WhoAmI.java
    +++ b/core/src/main/java/hudson/security/WhoAmI.java
    @@ -8,6 +8,7 @@ import hudson.model.UnprotectedRootAction;
     import java.util.ArrayList;
     import java.util.List;
     
    +import jenkins.util.MemoryReductionUtil;
     import jenkins.model.Jenkins;
     
     import org.acegisecurity.Authentication;
    @@ -62,7 +63,7 @@ public class WhoAmI implements UnprotectedRootAction {
         @Exported
         public String[] getAuthorities() {
             if (auth().getAuthorities() == null) {
    -            return new String[0];
    +            return MemoryReductionUtil.EMPTY_STRING_ARRAY;
             }
             List <String> authorities = new ArrayList<String>();
             for (GrantedAuthority a : auth().getAuthorities()) {
    diff --git a/core/src/main/java/hudson/security/captcha/CaptchaSupport.java b/core/src/main/java/hudson/security/captcha/CaptchaSupport.java
    index d5edf956637e3778998a3ec0e9c538c080ba6037..fb39e26e59b5754630036a29423f2cdf69a89fa7 100644
    --- a/core/src/main/java/hudson/security/captcha/CaptchaSupport.java
    +++ b/core/src/main/java/hudson/security/captcha/CaptchaSupport.java
    @@ -38,7 +38,7 @@ import jenkins.model.Jenkins;
      * Extension point for adding Captcha Support to User Registration Page {@link CaptchaSupport}.
      *
      * <p>
    - * This object can have an optional <tt>config.jelly</tt> to configure the Captcha Support
    + * This object can have an optional {@code config.jelly} to configure the Captcha Support
      * <p>
      * A default constructor is needed to create CaptchaSupport in
      * the default configuration.
    diff --git a/core/src/main/java/hudson/security/csrf/CrumbFilter.java b/core/src/main/java/hudson/security/csrf/CrumbFilter.java
    index 557e96f7662da77f6259a8e4e5b19a109d882a33..437d2209520b8ce5d4310f1c94b2380f7a7530bc 100644
    --- a/core/src/main/java/hudson/security/csrf/CrumbFilter.java
    +++ b/core/src/main/java/hudson/security/csrf/CrumbFilter.java
    @@ -7,6 +7,12 @@ package hudson.security.csrf;
     
     import hudson.util.MultipartFormDataParser;
     import jenkins.model.Jenkins;
    +import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
    +import org.kohsuke.MetaInfServices;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +import org.kohsuke.stapler.ForwardToView;
    +import org.kohsuke.stapler.interceptor.RequirePOST;
     
     import java.io.IOException;
     import java.util.Enumeration;
    @@ -40,6 +46,15 @@ public class CrumbFilter implements Filter {
             return h.getCrumbIssuer();
         }
     
    +    @Restricted(NoExternalUse.class)
    +    @MetaInfServices
    +    public static class ErrorCustomizer implements RequirePOST.ErrorCustomizer {
    +        @Override
    +        public ForwardToView getForwardView() {
    +            return new ForwardToView(CrumbFilter.class, "retry");
    +        }
    +    }
    +
         public void init(FilterConfig filterConfig) throws ServletException {
         }
     
    @@ -68,18 +83,22 @@ public class CrumbFilter implements Filter {
                     // compatibility for clients that hard-code the default crumb name up to Jenkins 1.TODO
                     extractCrumbFromRequest(httpRequest, ".crumb");
                 }
    +
    +            // JENKINS-40344: Don't spam the log just because a session is expired
    +            Level level = Jenkins.getAuthentication() instanceof AnonymousAuthenticationToken ? Level.FINE : Level.WARNING;
    +
                 if (crumb != null) {
                     if (crumbIssuer.validateCrumb(httpRequest, crumbSalt, crumb)) {
                         valid = true;
                     } else {
    -                    LOGGER.log(Level.WARNING, "Found invalid crumb {0}.  Will check remaining parameters for a valid one...", crumb);
    +                    LOGGER.log(level, "Found invalid crumb {0}.  Will check remaining parameters for a valid one...", crumb);
                     }
                 }
     
                 if (valid) {
                     chain.doFilter(request, response);
                 } else {
    -                LOGGER.log(Level.WARNING, "No valid crumb was included in request for {0}. Returning {1}.", new Object[] {httpRequest.getRequestURI(), HttpServletResponse.SC_FORBIDDEN});
    +                LOGGER.log(level, "No valid crumb was included in request for {0} by {1}. Returning {2}.", new Object[] {httpRequest.getRequestURI(), Jenkins.getAuthentication().getName(), HttpServletResponse.SC_FORBIDDEN});
                     httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN,"No valid crumb was included in the request");
                 }
             } else {
    diff --git a/core/src/main/java/hudson/security/csrf/CrumbIssuer.java b/core/src/main/java/hudson/security/csrf/CrumbIssuer.java
    index 349e69a136b62152d767aadf1ba429d1d275841c..a16f48689a40f19ab6900837b267ddc7110b6603 100644
    --- a/core/src/main/java/hudson/security/csrf/CrumbIssuer.java
    +++ b/core/src/main/java/hudson/security/csrf/CrumbIssuer.java
    @@ -9,6 +9,7 @@ import javax.servlet.ServletRequest;
     
     import hudson.init.Initializer;
     import jenkins.model.Jenkins;
    +import jenkins.security.stapler.StaplerAccessibleType;
     import org.kohsuke.stapler.Stapler;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.WebApp;
    @@ -40,6 +41,7 @@ import org.kohsuke.stapler.StaplerResponse;
      * @see <a href="http://en.wikipedia.org/wiki/XSRF">Wikipedia: Cross site request forgery</a>
      */
     @ExportedBean
    +@StaplerAccessibleType
     public abstract class CrumbIssuer implements Describable<CrumbIssuer>, ExtensionPoint {
     
         private static final String CRUMB_ATTRIBUTE = CrumbIssuer.class.getName() + "_crumb";
    @@ -193,6 +195,7 @@ public abstract class CrumbIssuer implements Describable<CrumbIssuer>, Extension
             }
     
             @Override public void doXml(StaplerRequest req, StaplerResponse rsp, @QueryParameter String xpath, @QueryParameter String wrapper, @QueryParameter String tree, @QueryParameter int depth) throws IOException, ServletException {
    +            setHeaders(rsp);
                 String text;
                 CrumbIssuer ci = (CrumbIssuer) bean;
                 if ("/*/crumbRequestField/text()".equals(xpath)) { // old FullDuplexHttpStream
    diff --git a/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java b/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java
    index 7f833c17f3263f7afa4e6ba255702cf049509f30..ae4a4edfab7272adabcf704ed252267e2ac08845 100644
    --- a/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java
    +++ b/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java
    @@ -12,6 +12,7 @@ import java.util.logging.Level;
     import java.util.logging.Logger;
     
     import hudson.Extension;
    +import hudson.model.PersistentDescriptor;
     import jenkins.util.SystemProperties;
     import hudson.Util;
     import jenkins.model.Jenkins;
    @@ -121,13 +122,12 @@ public class DefaultCrumbIssuer extends CrumbIssuer {
         }
         
         @Extension @Symbol("standard")
    -    public static final class DescriptorImpl extends CrumbIssuerDescriptor<DefaultCrumbIssuer> implements ModelObject {
    +    public static final class DescriptorImpl extends CrumbIssuerDescriptor<DefaultCrumbIssuer> implements ModelObject, PersistentDescriptor {
     
             private final static HexStringConfidentialKey CRUMB_SALT = new HexStringConfidentialKey(Jenkins.class,"crumbSalt",16);
             
             public DescriptorImpl() {
                 super(CRUMB_SALT.get(), SystemProperties.getString("hudson.security.csrf.requestfield", CrumbIssuer.DEFAULT_CRUMB_NAME));
    -            load();
             }
     
             @Override
    diff --git a/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java b/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java
    index a93c70cc23630bfb4e219ff4b11c6005bab13614..914fba876d4187485068202f2add0a2d8321cfff 100644
    --- a/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java
    +++ b/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java
    @@ -31,6 +31,8 @@ import net.sf.json.JSONObject;
     import org.jenkinsci.Symbol;
     import org.kohsuke.stapler.StaplerRequest;
     
    +import javax.annotation.Nonnull;
    +
     /**
      * Show the crumb configuration to the system config page.
      *
    @@ -39,14 +41,14 @@ import org.kohsuke.stapler.StaplerRequest;
     @Extension(ordinal=195) @Symbol("crumb") // immediately after the security setting
     public class GlobalCrumbIssuerConfiguration extends GlobalConfiguration {
         @Override
    -    public GlobalConfigurationCategory getCategory() {
    +    public @Nonnull GlobalConfigurationCategory getCategory() {
             return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
         }
     
         @Override
         public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
             // for compatibility reasons, the actual value is stored in Jenkins
    -        Jenkins j = Jenkins.getInstance();
    +        Jenkins j = Jenkins.get();
             if (json.has("csrf")) {
                 JSONObject csrf = json.getJSONObject("csrf");
                 j.setCrumbIssuer(CrumbIssuer.all().newInstanceFromRadioList(csrf, "issuer"));
    diff --git a/core/src/main/java/hudson/slaves/AbstractCloudSlave.java b/core/src/main/java/hudson/slaves/AbstractCloudSlave.java
    index 93f0f93b0c9a946ba6939bb1d4b141abbb0103f5..180d5c73867040d8cba715dc3b24a50c1c40a2bc 100644
    --- a/core/src/main/java/hudson/slaves/AbstractCloudSlave.java
    +++ b/core/src/main/java/hudson/slaves/AbstractCloudSlave.java
    @@ -67,7 +67,7 @@ public abstract class AbstractCloudSlave extends Slave {
                 _terminate(new StreamTaskListener(System.out, Charset.defaultCharset()));
             } finally {
                 try {
    -                Jenkins.getInstance().removeNode(this);
    +                Jenkins.get().removeNode(this);
                 } catch (IOException e) {
                     LOGGER.log(Level.WARNING, "Failed to remove "+name,e);
                 }
    diff --git a/core/src/main/java/hudson/slaves/ChannelPinger.java b/core/src/main/java/hudson/slaves/ChannelPinger.java
    index 0d4dae702ca82e6daaf5f487ffc8a392c60ff50a..8493fe13fb9945e96a7d8400996f224c656322ab 100644
    --- a/core/src/main/java/hudson/slaves/ChannelPinger.java
    +++ b/core/src/main/java/hudson/slaves/ChannelPinger.java
    @@ -23,6 +23,7 @@
      */
     package hudson.slaves;
     
    +import com.google.common.annotations.VisibleForTesting;
     import hudson.Extension;
     import hudson.FilePath;
     import jenkins.util.SystemProperties;
    @@ -33,14 +34,17 @@ import hudson.remoting.Channel;
     import hudson.remoting.PingThread;
     import jenkins.security.MasterToSlaveCallable;
     import jenkins.slaves.PingFailureAnalyzer;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
    +import javax.annotation.CheckForNull;
     import java.io.IOException;
     import java.util.concurrent.atomic.AtomicBoolean;
     import java.util.logging.Level;
     import java.util.logging.Logger;
     
     /**
    - * Establish a periodic ping to keep connections between {@link Slave slaves}
    + * Establish a periodic ping to keep connections between {@link Slave agents}
      * and the main Jenkins node alive. This prevents network proxies from
      * terminating connections that are idle for too long.
      *
    @@ -86,42 +90,54 @@ public class ChannelPinger extends ComputerListener {
     
         @Override
         public void preOnline(Computer c, Channel channel, FilePath root, TaskListener listener)  {
    -        install(channel);
    +        SlaveComputer slaveComputer = null;
    +        if (c instanceof SlaveComputer) {
    +            slaveComputer = (SlaveComputer) c;
    +        }
    +        install(channel, slaveComputer);
         }
     
         public void install(Channel channel) {
    +        install(channel, null);
    +    }
    +
    +    @VisibleForTesting
    +    /*package*/ void install(Channel channel, @CheckForNull SlaveComputer c) {
             if (pingTimeoutSeconds < 1 || pingIntervalSeconds < 1) {
                 LOGGER.warning("Agent ping is disabled");
                 return;
             }
     
    +        // set up ping from both directions, so that in case of a router dropping a connection,
    +        // both sides can notice it and take compensation actions.
             try {
                 channel.call(new SetUpRemotePing(pingTimeoutSeconds, pingIntervalSeconds));
                 LOGGER.fine("Set up a remote ping for " + channel.getName());
             } catch (Exception e) {
    -            LOGGER.severe("Failed to set up a ping for " + channel.getName());
    +            LOGGER.log(Level.SEVERE, "Failed to set up a ping for " + channel.getName(), e);
             }
     
    -        // set up ping from both directions, so that in case of a router dropping a connection,
    -        // both sides can notice it and take compensation actions.
    -        setUpPingForChannel(channel, pingTimeoutSeconds, pingIntervalSeconds, true);
    +        setUpPingForChannel(channel, c, pingTimeoutSeconds, pingIntervalSeconds, true);
         }
     
    -    static class SetUpRemotePing extends MasterToSlaveCallable<Void, IOException> {
    +    @VisibleForTesting
    +    @Restricted(NoExternalUse.class)
    +    public static class SetUpRemotePing extends MasterToSlaveCallable<Void, IOException> {
             private static final long serialVersionUID = -2702219700841759872L;
             @Deprecated
             private transient int pingInterval;
             private final int pingTimeoutSeconds;
             private final int pingIntervalSeconds;
     
    -        SetUpRemotePing(int pingTimeoutSeconds, int pingIntervalSeconds) {
    +        public SetUpRemotePing(int pingTimeoutSeconds, int pingIntervalSeconds) {
                 this.pingTimeoutSeconds = pingTimeoutSeconds;
                 this.pingIntervalSeconds = pingIntervalSeconds;
             }
     
             @Override
             public Void call() throws IOException {
    -            setUpPingForChannel(Channel.current(), pingTimeoutSeconds, pingIntervalSeconds, false);
    +            // No sense in setting up channel pinger if the channel is being closed
    +            setUpPingForChannel(getOpenChannelOrFail(), null, pingTimeoutSeconds, pingIntervalSeconds, false);
                 return null;
             }
     
    @@ -163,30 +179,37 @@ public class ChannelPinger extends ComputerListener {
             }
         }
     
    -    static void setUpPingForChannel(final Channel channel, int timeoutSeconds, int intervalSeconds, final boolean analysis) {
    +    @VisibleForTesting
    +    @Restricted(NoExternalUse.class)
    +    public static void setUpPingForChannel(final Channel channel, final SlaveComputer computer, int timeoutSeconds, int intervalSeconds, final boolean analysis) {
             LOGGER.log(Level.FINE, "setting up ping on {0} with a {1} seconds interval and {2} seconds timeout", new Object[] {channel.getName(), intervalSeconds, timeoutSeconds});
             final AtomicBoolean isInClosed = new AtomicBoolean(false);
             final PingThread t = new PingThread(channel, timeoutSeconds * 1000L, intervalSeconds * 1000L) {
                 @Override
                 protected void onDead(Throwable cause) {
    -                try {
                         if (analysis) {
                             analyze(cause);
                         }
    -                    if (isInClosed.get()) {
    +                    boolean inClosed = isInClosed.get();
    +                    // Disassociate computer channel before closing it
    +                    if (computer != null) {
    +                        Exception exception = cause instanceof Exception ? (Exception) cause: new IOException(cause);
    +                        computer.disconnect(new OfflineCause.ChannelTermination(exception));
    +                    }
    +                    if (inClosed) {
                             LOGGER.log(Level.FINE,"Ping failed after the channel "+channel.getName()+" is already partially closed.",cause);
                         } else {
                             LOGGER.log(Level.INFO,"Ping failed. Terminating the channel "+channel.getName()+".",cause);
    -                        channel.close(cause);
                         }
    -                } catch (IOException e) {
    -                    LOGGER.log(Level.SEVERE,"Failed to terminate the channel "+channel.getName(),e);
    -                }
                 }
                 /** Keep in a separate method so we do not even try to do class loading on {@link PingFailureAnalyzer} from an agent JVM. */
    -            private void analyze(Throwable cause) throws IOException {
    +            private void analyze(Throwable cause) {
                     for (PingFailureAnalyzer pfa : PingFailureAnalyzer.all()) {
    -                    pfa.onPingFailure(channel,cause);
    +                    try {
    +                        pfa.onPingFailure(channel, cause);
    +                    } catch (IOException ex) {
    +                        LOGGER.log(Level.WARNING, "Ping failure analyzer " + pfa.getClass().getName() + " failed for " + channel.getName(), ex);
    +                    }
                     }
                 }
                 @Deprecated
    diff --git a/core/src/main/java/hudson/slaves/Channels.java b/core/src/main/java/hudson/slaves/Channels.java
    index 873cdbe4e3bb8a1754338d02b1506474dea85cdb..4b0b910b37818cd97236dd830e53f67d94a7e051 100644
    --- a/core/src/main/java/hudson/slaves/Channels.java
    +++ b/core/src/main/java/hudson/slaves/Channels.java
    @@ -164,7 +164,7 @@ public class Channels {
          * @param workDir
          *      If non-null, the new JVM will have this directory as the working directory. This must be a local path.
          * @param classpath
    -     *      The classpath of the new JVM. Can be null if you just need {@code slave.jar} (and everything else
    +     *      The classpath of the new JVM. Can be null if you just need {@code agent.jar} (and everything else
          *      can be sent over the channel.) But if you have jars that are known to be necessary by the new JVM,
          *      setting it here will improve the classloading performance (by avoiding remote class file transfer.)
          *      Classes in this classpath will also take precedence over any other classes that's sent via the channel
    @@ -195,7 +195,7 @@ public class Channels {
          * @param workDir
          *      If non-null, the new JVM will have this directory as the working directory. This must be a local path.
          * @param classpath
    -     *      The classpath of the new JVM. Can be null if you just need {@code slave.jar} (and everything else
    +     *      The classpath of the new JVM. Can be null if you just need {@code agent.jar} (and everything else
          *      can be sent over the channel.) But if you have jars that are known to be necessary by the new JVM,
          *      setting it here will improve the classloading performance (by avoiding remote class file transfer.)
          *      Classes in this classpath will also take precedence over any other classes that's sent via the channel
    diff --git a/core/src/main/java/hudson/slaves/Cloud.java b/core/src/main/java/hudson/slaves/Cloud.java
    index c1bc5d7d89767a683f478dca5762ba273b0d5bc7..df718ccbd78bb455cf3498fa93d65fa08103c2c2 100644
    --- a/core/src/main/java/hudson/slaves/Cloud.java
    +++ b/core/src/main/java/hudson/slaves/Cloud.java
    @@ -34,7 +34,6 @@ import hudson.slaves.NodeProvisioner.PlannedNode;
     import hudson.model.Describable;
     import jenkins.model.Jenkins;
     import hudson.model.Node;
    -import hudson.model.AbstractModelObject;
     import hudson.model.Label;
     import hudson.model.Descriptor;
     import hudson.security.ACL;
    @@ -67,7 +66,7 @@ import java.util.concurrent.Future;
      * <p>
      * To do this, have your {@link Slave} subtype remember the necessary handle (such as EC2 instance ID)
      * as a field. Such fields need to survive the user-initiated re-configuration of {@link Slave}, so you'll need to
    - * expose it in your {@link Slave} <tt>configure-entries.jelly</tt> and read it back in through {@link DataBoundConstructor}.
    + * expose it in your {@link Slave} {@code configure-entries.jelly} and read it back in through {@link DataBoundConstructor}.
      *
      * <p>
      * You then implement your own {@link Computer} subtype, override {@link Slave#createComputer()}, and instantiate
    @@ -80,9 +79,9 @@ import java.util.concurrent.Future;
      *
      * <h3>Views</h3>
      *
    - * Since version TODO, Jenkins clouds are visualized in UI. Implementations can provide <tt>top</tt> or <tt>main</tt> view
    - * to be presented at the top of the page or at the bottom respectively. In the middle, actions have their <tt>summary</tt>
    - * views displayed. Actions further contribute to <tt>sidepanel</tt> with <tt>box</tt> views. All mentioned views are
    + * Since version 2.64, Jenkins clouds are visualized in UI. Implementations can provide {@code top} or {@code main} view
    + * to be presented at the top of the page or at the bottom respectively. In the middle, actions have their {@code summary}
    + * views displayed. Actions further contribute to {@code sidepanel} with {@code box} views. All mentioned views are
      * optional to preserve backward compatibility.
      *
      * @author Kohsuke Kawaguchi
    @@ -110,7 +109,7 @@ public abstract class Cloud extends Actionable implements ExtensionPoint, Descri
         /**
          * Get URL of the cloud.
          *
    -     * @since TODO
    +     * @since 2.64
          * @return Jenkins relative URL.
          */
         public @Nonnull String getUrl() {
    @@ -125,15 +124,7 @@ public abstract class Cloud extends Actionable implements ExtensionPoint, Descri
         }
     
         public ACL getACL() {
    -        return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
    -    }
    -
    -    public final void checkPermission(Permission permission) {
    -        getACL().checkPermission(permission);
    -    }
    -
    -    public final boolean hasPermission(Permission permission) {
    -        return getACL().hasPermission(permission);
    +        return Jenkins.get().getAuthorizationStrategy().getACL(this);
         }
     
         /**
    @@ -177,7 +168,7 @@ public abstract class Cloud extends Actionable implements ExtensionPoint, Descri
         public abstract boolean canProvision(Label label);
     
         public Descriptor<Cloud> getDescriptor() {
    -        return Jenkins.getInstance().getDescriptorOrDie(getClass());
    +        return Jenkins.get().getDescriptorOrDie(getClass());
         }
     
         /**
    @@ -187,13 +178,13 @@ public abstract class Cloud extends Actionable implements ExtensionPoint, Descri
          *      Use {@link #all()} for read access, and {@link Extension} for registration.
          */
         @Deprecated
    -    public static final DescriptorList<Cloud> ALL = new DescriptorList<Cloud>(Cloud.class);
    +    public static final DescriptorList<Cloud> ALL = new DescriptorList<>(Cloud.class);
     
         /**
          * Returns all the registered {@link Cloud} descriptors.
          */
         public static DescriptorExtensionList<Cloud,Descriptor<Cloud>> all() {
    -        return Jenkins.getInstance().<Cloud,Descriptor<Cloud>>getDescriptorList(Cloud.class);
    +        return Jenkins.get().getDescriptorList(Cloud.class);
         }
     
         private static final PermissionScope PERMISSION_SCOPE = new PermissionScope(Cloud.class);
    diff --git a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java
    index db2b80cf455abbeb2fa939776ed5f418268fa805..852f38d6893adc80cbebda51434506a1e3de533e 100644
    --- a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java
    +++ b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java
    @@ -29,7 +29,7 @@ import javax.annotation.concurrent.GuardedBy;
     import java.io.IOException;
     import java.util.logging.Logger;
     
    -import static hudson.util.TimeUnit2.*;
    +import static java.util.concurrent.TimeUnit.*;
     import java.util.logging.Level;
     import static java.util.logging.Level.*;
     
    @@ -57,9 +57,7 @@ public class CloudRetentionStrategy extends RetentionStrategy<AbstractCloudCompu
                     LOGGER.log(Level.INFO, "Disconnecting {0}", c.getName());
                     try {
                         computerNode.terminate();
    -                } catch (InterruptedException e) {
    -                    LOGGER.log(WARNING, "Failed to terminate " + c.getName(), e);
    -                } catch (IOException e) {
    +                } catch (InterruptedException | IOException e) {
                         LOGGER.log(WARNING, "Failed to terminate " + c.getName(), e);
                     }
                 }
    diff --git a/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java
    index accfca80c3a4242756984095d9d6961fb1766617..e14d934801f9698629ea23d3a4ab12691844f538 100644
    --- a/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java
    +++ b/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java
    @@ -2,7 +2,7 @@ package hudson.slaves;
     
     import hudson.model.Computer;
     import hudson.model.Node;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import jenkins.model.Jenkins;
     
     import javax.annotation.concurrent.GuardedBy;
    @@ -47,7 +47,7 @@ public class CloudSlaveRetentionStrategy<T extends Computer> extends RetentionSt
          * To actually deallocate the resource tied to this {@link Node}, implement {@link Computer#onRemoved()}.
          */
         protected void kill(Node n) throws IOException {
    -        Jenkins.getInstance().removeNode(n);
    +        Jenkins.get().removeNode(n);
         }
     
         /**
    @@ -72,7 +72,7 @@ public class CloudSlaveRetentionStrategy<T extends Computer> extends RetentionSt
         }
     
         // for debugging, it's convenient to be able to reduce this time
    -    public static long TIMEOUT = SystemProperties.getLong(CloudSlaveRetentionStrategy.class.getName()+".timeout", TimeUnit2.MINUTES.toMillis(10));
    +    public static long TIMEOUT = SystemProperties.getLong(CloudSlaveRetentionStrategy.class.getName()+".timeout", TimeUnit.MINUTES.toMillis(10));
     
         private static final Logger LOGGER = Logger.getLogger(CloudSlaveRetentionStrategy.class.getName());
     }
    diff --git a/core/src/main/java/hudson/slaves/CommandLauncher.java b/core/src/main/java/hudson/slaves/CommandLauncher.java
    deleted file mode 100644
    index 6a1d4ce3e745cc4db20b3791f2025ba16fb2fd41..0000000000000000000000000000000000000000
    --- a/core/src/main/java/hudson/slaves/CommandLauncher.java
    +++ /dev/null
    @@ -1,201 +0,0 @@
    -/*
    - * The MIT License
    - *
    - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly
    - *
    - * 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.slaves;
    -
    -import hudson.AbortException;
    -import hudson.EnvVars;
    -import hudson.Util;
    -import hudson.Extension;
    -import hudson.Functions;
    -import hudson.model.Descriptor;
    -import hudson.model.Slave;
    -import jenkins.model.Jenkins;
    -import hudson.model.TaskListener;
    -import hudson.remoting.Channel;
    -import hudson.util.StreamCopyThread;
    -import hudson.util.FormValidation;
    -import hudson.util.ProcessTree;
    -
    -import java.io.IOException;
    -import java.util.Date;
    -import java.util.logging.Level;
    -import java.util.logging.Logger;
    -
    -import org.apache.commons.lang.StringUtils;
    -import org.jenkinsci.Symbol;
    -import org.kohsuke.stapler.DataBoundConstructor;
    -import org.kohsuke.stapler.QueryParameter;
    -
    -/**
    - * {@link ComputerLauncher} through a remote login mechanism like ssh/rsh.
    - *
    - * @author Stephen Connolly
    - * @author Kohsuke Kawaguchi
    -*/
    -public class CommandLauncher extends ComputerLauncher {
    -
    -    /**
    -     * Command line to launch the agent, like
    -     * "ssh myslave java -jar /path/to/hudson-remoting.jar"
    -     */
    -    private final String agentCommand;
    -
    -    /**
    -     * Optional environment variables to add to the current environment. Can be null.
    -     */
    -    private final EnvVars env;
    -
    -    @DataBoundConstructor
    -    public CommandLauncher(String command) {
    -        this(command, null);
    -    }
    -
    -    public CommandLauncher(String command, EnvVars env) {
    -    	this.agentCommand = command;
    -    	this.env = env;
    -    }
    -
    -    public String getCommand() {
    -        return agentCommand;
    -    }
    -
    -    /**
    -     * Gets the formatted current time stamp.
    -     */
    -    private static String getTimestamp() {
    -        return String.format("[%1$tD %1$tT]", new Date());
    -    }
    -
    -    @Override
    -    public void launch(SlaveComputer computer, final TaskListener listener) {
    -        EnvVars _cookie = null;
    -        Process _proc = null;
    -        try {
    -            Slave node = computer.getNode();
    -            if (node == null) {
    -                throw new AbortException("Cannot launch commands on deleted nodes");
    -            }
    -
    -            listener.getLogger().println(hudson.model.Messages.Slave_Launching(getTimestamp()));
    -            if(getCommand().trim().length()==0) {
    -                listener.getLogger().println(Messages.CommandLauncher_NoLaunchCommand());
    -                return;
    -            }
    -            listener.getLogger().println("$ " + getCommand());
    -
    -            ProcessBuilder pb = new ProcessBuilder(Util.tokenize(getCommand()));
    -            final EnvVars cookie = _cookie = EnvVars.createCookie();
    -            pb.environment().putAll(cookie);
    -            pb.environment().put("WORKSPACE", StringUtils.defaultString(computer.getAbsoluteRemoteFs(), node.getRemoteFS())); //path for local slave log
    -
    -            {// system defined variables
    -                String rootUrl = Jenkins.getInstance().getRootUrl();
    -                if (rootUrl!=null) {
    -                    pb.environment().put("HUDSON_URL", rootUrl);    // for backward compatibility
    -                    pb.environment().put("JENKINS_URL", rootUrl);
    -                    pb.environment().put("SLAVEJAR_URL", rootUrl+"/jnlpJars/slave.jar");
    -                }
    -            }
    -
    -            if (env != null) {
    -            	pb.environment().putAll(env);
    -            }
    -
    -            final Process proc = _proc = pb.start();
    -
    -            // capture error information from stderr. this will terminate itself
    -            // when the process is killed.
    -            new StreamCopyThread("stderr copier for remote agent on " + computer.getDisplayName(),
    -                    proc.getErrorStream(), listener.getLogger()).start();
    -
    -            computer.setChannel(proc.getInputStream(), proc.getOutputStream(), listener.getLogger(), new Channel.Listener() {
    -                @Override
    -                public void onClosed(Channel channel, IOException cause) {
    -                    reportProcessTerminated(proc, listener);
    -
    -                    try {
    -                        ProcessTree.get().killAll(proc, cookie);
    -                    } catch (InterruptedException e) {
    -                        LOGGER.log(Level.INFO, "interrupted", e);
    -                    }
    -                }
    -            });
    -
    -            LOGGER.info("agent launched for " + computer.getDisplayName());
    -        } catch (InterruptedException e) {
    -            Functions.printStackTrace(e, listener.error(Messages.ComputerLauncher_abortedLaunch()));
    -        } catch (RuntimeException e) {
    -            Functions.printStackTrace(e, listener.error(Messages.ComputerLauncher_unexpectedError()));
    -        } catch (Error e) {
    -            Functions.printStackTrace(e, listener.error(Messages.ComputerLauncher_unexpectedError()));
    -        } catch (IOException e) {
    -            Util.displayIOException(e, listener);
    -
    -            String msg = Util.getWin32ErrorMessage(e);
    -            if (msg == null) {
    -                msg = "";
    -            } else {
    -                msg = " : " + msg;
    -                // FIXME TODO i18n what is this!?
    -            }
    -            msg = hudson.model.Messages.Slave_UnableToLaunch(computer.getDisplayName(), msg);
    -            LOGGER.log(Level.SEVERE, msg, e);
    -            Functions.printStackTrace(e, listener.error(msg));
    -
    -            if(_proc!=null) {
    -                reportProcessTerminated(_proc, listener);
    -                try {
    -                    ProcessTree.get().killAll(_proc, _cookie);
    -                } catch (InterruptedException x) {
    -                    Functions.printStackTrace(x, listener.error(Messages.ComputerLauncher_abortedLaunch()));
    -                }
    -            }
    -        }
    -    }
    -
    -    private static void reportProcessTerminated(Process proc, TaskListener listener) {
    -        try {
    -            int exitCode = proc.exitValue();
    -            listener.error("Process terminated with exit code " + exitCode);
    -        } catch (IllegalThreadStateException e) {
    -            // hasn't terminated yet
    -        }
    -    }
    -
    -    private static final Logger LOGGER = Logger.getLogger(CommandLauncher.class.getName());
    -
    -    @Extension @Symbol("command")
    -    public static class DescriptorImpl extends Descriptor<ComputerLauncher> {
    -        public String getDisplayName() {
    -            return Messages.CommandLauncher_displayName();
    -        }
    -
    -        public FormValidation doCheckCommand(@QueryParameter String value) {
    -            if(Util.fixEmptyAndTrim(value)==null)
    -                return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand());
    -            else
    -                return FormValidation.ok();
    -        }
    -    }
    -}
    diff --git a/core/src/main/java/hudson/slaves/ComputerConnectorDescriptor.java b/core/src/main/java/hudson/slaves/ComputerConnectorDescriptor.java
    index c21a68b8d4591a3996ad5efb8226734e7ea94139..6d21bb9623bd359ae01565552f91f77c848738e0 100644
    --- a/core/src/main/java/hudson/slaves/ComputerConnectorDescriptor.java
    +++ b/core/src/main/java/hudson/slaves/ComputerConnectorDescriptor.java
    @@ -12,7 +12,6 @@ import jenkins.model.Jenkins;
      */
     public abstract class ComputerConnectorDescriptor extends Descriptor<ComputerConnector> {
         public static DescriptorExtensionList<ComputerConnector,ComputerConnectorDescriptor> all() {
    -        return Jenkins.getInstance().<ComputerConnector,ComputerConnectorDescriptor>
    -                                    getDescriptorList(ComputerConnector.class);
    +        return Jenkins.get().getDescriptorList(ComputerConnector.class);
         }
     }
    diff --git a/core/src/main/java/hudson/slaves/ComputerLauncher.java b/core/src/main/java/hudson/slaves/ComputerLauncher.java
    index 603df116de66f8466fd6a7962547430cc85c1ab4..6a33b533843760ca9b97fc5a33f7913163da72f1 100644
    --- a/core/src/main/java/hudson/slaves/ComputerLauncher.java
    +++ b/core/src/main/java/hudson/slaves/ComputerLauncher.java
    @@ -48,7 +48,7 @@ import org.apache.tools.ant.util.DeweyDecimal;
      * </dl>
      *
      * @author Stephen Connolly
    - * @since 24-Apr-2008 22:12:35
    + * @since 1.216-ish
      * @see ComputerConnector
      */
     public abstract class ComputerLauncher extends AbstractDescribableImpl<ComputerLauncher> implements ExtensionPoint {
    @@ -163,7 +163,7 @@ public abstract class ComputerLauncher extends AbstractDescribableImpl<ComputerL
          *      {@link jenkins.model.Jenkins#getDescriptorList(Class)} for read access.
          */
         @Deprecated
    -    public static final DescriptorList<ComputerLauncher> LIST = new DescriptorList<ComputerLauncher>(ComputerLauncher.class);
    +    public static final DescriptorList<ComputerLauncher> LIST = new DescriptorList<>(ComputerLauncher.class);
     
         /**
          * Given the output of "java -version" in <code>r</code>, determine if this
    @@ -187,7 +187,7 @@ public abstract class ComputerLauncher extends AbstractDescribableImpl<ComputerL
                     final String versionStr = m.group(1);
                     logger.println(Messages.ComputerLauncher_JavaVersionResult(javaCommand, versionStr));
                     try {
    -                    if (new DeweyDecimal(versionStr).isLessThan(new DeweyDecimal("1.6"))) {
    +                    if (new DeweyDecimal(versionStr).isLessThan(new DeweyDecimal("1.8"))) {
                             throw new IOException(Messages
                                     .ComputerLauncher_NoJavaFound(line));
                         }
    diff --git a/core/src/main/java/hudson/slaves/ComputerRetentionWork.java b/core/src/main/java/hudson/slaves/ComputerRetentionWork.java
    index 8b33592b2261652973a22c8f69ce4808a15d9889..36ec97ab3b6b655a5c5e8da9d5396881c48ddfa4 100644
    --- a/core/src/main/java/hudson/slaves/ComputerRetentionWork.java
    +++ b/core/src/main/java/hudson/slaves/ComputerRetentionWork.java
    @@ -46,7 +46,7 @@ public class ComputerRetentionWork extends PeriodicWork {
         /**
          * Use weak hash map to avoid leaking {@link Computer}.
          */
    -    private final Map<Computer, Long> nextCheck = new WeakHashMap<Computer, Long>();
    +    private final Map<Computer, Long> nextCheck = new WeakHashMap<>();
     
         public long getRecurrencePeriod() {
             return MIN;
    @@ -59,7 +59,7 @@ public class ComputerRetentionWork extends PeriodicWork {
         @Override
         protected void doRun() {
             final long startRun = System.currentTimeMillis();
    -        for (final Computer c : Jenkins.getInstance().getComputers()) {
    +        for (final Computer c : Jenkins.get().getComputers()) {
                 Queue.withLock(new Runnable() {
                     @Override
                     public void run() {
    diff --git a/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java b/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java
    index 4bd542d7de3d1e2c77e3024cce7f862c7d590eb0..e4a4ec2f044bc16a756ef54ca6e7058c2fd0a5fa 100644
    --- a/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java
    +++ b/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java
    @@ -27,7 +27,7 @@ import hudson.model.AsyncPeriodicWork;
     import hudson.model.TaskListener;
     import jenkins.model.Jenkins;
     import hudson.model.Computer;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import hudson.remoting.VirtualChannel;
     import hudson.remoting.Channel;
     import hudson.Extension;
    @@ -58,7 +58,7 @@ public class ConnectionActivityMonitor extends AsyncPeriodicWork {
             if (!enabled)   return;
     
             long now = System.currentTimeMillis();
    -        for (Computer c: Jenkins.getInstance().getComputers()) {
    +        for (Computer c: Jenkins.get().getComputers()) {
                 VirtualChannel ch = c.getChannel();
                 if (ch instanceof Channel) {
                     Channel channel = (Channel) ch;
    @@ -84,20 +84,20 @@ public class ConnectionActivityMonitor extends AsyncPeriodicWork {
         }
     
         public long getRecurrencePeriod() {
    -        return enabled ? FREQUENCY : TimeUnit2.DAYS.toMillis(30);
    +        return enabled ? FREQUENCY : TimeUnit.DAYS.toMillis(30);
         }
     
         /**
          * Time till initial ping
          */
    -    private static final long TIME_TILL_PING = SystemProperties.getLong(ConnectionActivityMonitor.class.getName()+".timeToPing",TimeUnit2.MINUTES.toMillis(3));
    +    private static final long TIME_TILL_PING = SystemProperties.getLong(ConnectionActivityMonitor.class.getName()+".timeToPing",TimeUnit.MINUTES.toMillis(3));
     
    -    private static final long FREQUENCY = SystemProperties.getLong(ConnectionActivityMonitor.class.getName()+".frequency",TimeUnit2.SECONDS.toMillis(10));
    +    private static final long FREQUENCY = SystemProperties.getLong(ConnectionActivityMonitor.class.getName()+".frequency",TimeUnit.SECONDS.toMillis(10));
     
         /**
          * When do we abandon the effort and cut off?
          */
    -    private static final long TIMEOUT = SystemProperties.getLong(ConnectionActivityMonitor.class.getName()+".timeToPing",TimeUnit2.MINUTES.toMillis(4));
    +    private static final long TIMEOUT = SystemProperties.getLong(ConnectionActivityMonitor.class.getName()+".timeToPing",TimeUnit.MINUTES.toMillis(4));
     
     
         // disabled by default until proven in the production
    diff --git a/core/src/main/java/hudson/slaves/DelegatingComputerLauncher.java b/core/src/main/java/hudson/slaves/DelegatingComputerLauncher.java
    index 44b18b4f6b0cceea2df137fc42018f36ffbee8b8..79243ea5e3fb7bf23baf7487d7ab9cef41ef7360 100644
    --- a/core/src/main/java/hudson/slaves/DelegatingComputerLauncher.java
    +++ b/core/src/main/java/hudson/slaves/DelegatingComputerLauncher.java
    @@ -100,7 +100,7 @@ public abstract class DelegatingComputerLauncher extends ComputerLauncher {
             public List<Descriptor<ComputerLauncher>> getApplicableDescriptors() {
                 List<Descriptor<ComputerLauncher>> r = new ArrayList<Descriptor<ComputerLauncher>>();
                 for (Descriptor<ComputerLauncher> d :
    -                    Jenkins.getInstance().<ComputerLauncher, Descriptor<ComputerLauncher>>getDescriptorList(ComputerLauncher.class)) {
    +                    Jenkins.get().getDescriptorList(ComputerLauncher.class)) {
                     if (DelegatingComputerLauncher.class.isAssignableFrom(d.getKlass().toJavaClass()))  continue;
                     r.add(d);
                 }
    diff --git a/core/src/main/java/hudson/slaves/DumbSlave.java b/core/src/main/java/hudson/slaves/DumbSlave.java
    index 835cf004ad23d02fb8fc8de08468f2122084a295..f62937644fc11b1488eace25e3925e55b71645c9 100644
    --- a/core/src/main/java/hudson/slaves/DumbSlave.java
    +++ b/core/src/main/java/hudson/slaves/DumbSlave.java
    @@ -53,9 +53,10 @@ public final class DumbSlave extends Slave {
         }
         
         /**
    -     * @deprecated as of 1.XXX.
    +     * @deprecated as of 2.2.
          *      Use {@link #DumbSlave(String, String, ComputerLauncher)} and configure the rest through setters.
          */
    +    @Deprecated
         public DumbSlave(String name, String nodeDescription, String remoteFS, String numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, List<? extends NodeProperty<?>> nodeProperties) throws IOException, FormException {
         	super(name, nodeDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, nodeProperties);
         }
    @@ -65,8 +66,8 @@ public final class DumbSlave extends Slave {
             super(name, remoteFS, launcher);
         }
     
    -    @Extension @Symbol({"dumb",
    -            "slave"/*because this is in effect the canonical slave type*/})
    +    @Extension @Symbol({"permanent" /*because this is in effect the canonical slave type*/, 
    +            "dumb", "slave"})
         public static final class DescriptorImpl extends SlaveDescriptor {
             public String getDisplayName() {
                 return Messages.DumbSlave_displayName();
    diff --git a/core/src/main/java/hudson/slaves/EnvironmentVariablesNodeProperty.java b/core/src/main/java/hudson/slaves/EnvironmentVariablesNodeProperty.java
    index d41b8c49a12326d819a993dd3e923221004f88d5..90f95efb4e17fa0d773c7c274e905f7e079319fd 100644
    --- a/core/src/main/java/hudson/slaves/EnvironmentVariablesNodeProperty.java
    +++ b/core/src/main/java/hudson/slaves/EnvironmentVariablesNodeProperty.java
    @@ -39,6 +39,9 @@ import org.kohsuke.stapler.Stapler;
     import java.io.IOException;
     import java.util.Arrays;
     import java.util.List;
    +import java.util.Map;
    +import java.util.Set;
    +import java.util.stream.Collectors;
     
     /**
      * {@link NodeProperty} that sets additional environment variables.
    @@ -65,6 +68,14 @@ public class EnvironmentVariablesNodeProperty extends NodeProperty<Node> {
         	return envVars;
         }
     
    +    /**
    +     * @return environment variables using same data type as constructor parameter.
    +     * @since 2.136
    +     */
    +    public List<Entry> getEnv() {
    +        return envVars.entrySet().stream().map(Entry::new).collect(Collectors.toList());
    +    }
    +
         @Override
         public Environment setUp(AbstractBuild build, Launcher launcher,
     			BuildListener listener) throws IOException, InterruptedException {
    @@ -100,6 +111,10 @@ public class EnvironmentVariablesNodeProperty extends NodeProperty<Node> {
     	public static class Entry {
     		public String key, value;
     
    +		private Entry(Map.Entry<String,String> e) {
    +		    this(e.getKey(), e.getValue());
    +        }
    +
     		@DataBoundConstructor
     		public Entry(String key, String value) {
     			this.key = key;
    diff --git a/core/src/main/java/hudson/slaves/JNLPLauncher.java b/core/src/main/java/hudson/slaves/JNLPLauncher.java
    index c83382f796261c1283bb411fa4b90248665bf3ab..6c700acc5c2a7102e26e248e8e4834b424327be0 100644
    --- a/core/src/main/java/hudson/slaves/JNLPLauncher.java
    +++ b/core/src/main/java/hudson/slaves/JNLPLauncher.java
    @@ -25,14 +25,21 @@ package hudson.slaves;
     
     import hudson.Extension;
     import hudson.Util;
    +import hudson.model.Computer;
     import hudson.model.Descriptor;
     import hudson.model.DescriptorVisibilityFilter;
     import hudson.model.TaskListener;
     import javax.annotation.CheckForNull;
     import javax.annotation.Nonnull;
    +
     import jenkins.model.Jenkins;
    +import jenkins.slaves.RemotingWorkDirSettings;
    +import jenkins.util.java.JavaUtils;
     import org.jenkinsci.Symbol;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     import org.kohsuke.stapler.DataBoundConstructor;
    +import org.kohsuke.stapler.DataBoundSetter;
     
     /**
      * {@link ComputerLauncher} via JNLP.
    @@ -52,24 +59,85 @@ public class JNLPLauncher extends ComputerLauncher {
          *
          * @since 1.250
          */
    +    @CheckForNull
         public final String tunnel;
     
         /**
          * Additional JVM arguments. Can be null.
          * @since 1.297
          */
    +    @CheckForNull
         public final String vmargs;
     
    +    @Nonnull
    +    private RemotingWorkDirSettings workDirSettings = RemotingWorkDirSettings.getEnabledDefaults();
    +
    +    /**
    +     * Constructor.
    +     * @param tunnel Tunnel settings
    +     * @param vmargs JVM arguments
    +     * @param workDirSettings Settings for Work Directory management in Remoting.
    +     *                        If {@code null}, {@link RemotingWorkDirSettings#getEnabledDefaults()}
    +     *                        will be used to enable work directories by default in new agents.
    +     * @since 2.68
    +     */
    +    @Deprecated
    +    public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs, @CheckForNull RemotingWorkDirSettings workDirSettings) {
    +        this(tunnel, vmargs);
    +        if (workDirSettings != null) {
    +            setWorkDirSettings(workDirSettings);
    +        }
    +    }
    +    
         @DataBoundConstructor
    -    public JNLPLauncher(String tunnel, String vmargs) {
    +    public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs) {
             this.tunnel = Util.fixEmptyAndTrim(tunnel);
             this.vmargs = Util.fixEmptyAndTrim(vmargs);
         }
     
    +    /**
    +     * @deprecated This Launcher does not enable the work directory.
    +     *             It is recommended to use {@link #JNLPLauncher(boolean)}
    +     */
    +    @Deprecated
         public JNLPLauncher() {
    -        this(null,null);
    +        this(false);
    +    }
    +    
    +    /**
    +     * Constructor with default options.
    +     * 
    +     * @param enableWorkDir If {@code true}, the work directory will be enabled with default settings.
    +     */
    +    public JNLPLauncher(boolean enableWorkDir) {
    +        this(null, null, enableWorkDir 
    +                ? RemotingWorkDirSettings.getEnabledDefaults() 
    +                : RemotingWorkDirSettings.getDisabledDefaults());
    +    }
    +    
    +    protected Object readResolve() {
    +        if (workDirSettings == null) {
    +            // For the migrated code agents are always disabled
    +            workDirSettings = RemotingWorkDirSettings.getDisabledDefaults();
    +        }
    +        return this;
    +    }
    +
    +    /**
    +     * Returns work directory settings.
    +     * 
    +     * @since 2.72
    +     */
    +    @Nonnull
    +    public RemotingWorkDirSettings getWorkDirSettings() {
    +        return workDirSettings;
         }
     
    +    @DataBoundSetter
    +    public final void setWorkDirSettings(@Nonnull RemotingWorkDirSettings workDirSettings) {
    +        this.workDirSettings = workDirSettings;
    +    }
    +    
         @Override
         public boolean isLaunchSupported() {
             return false;
    @@ -86,6 +154,22 @@ public class JNLPLauncher extends ComputerLauncher {
          */
         public static /*almost final*/ Descriptor<ComputerLauncher> DESCRIPTOR;
     
    +    /**
    +     * Gets work directory options as a String.
    +     * 
    +     * In public API {@code getWorkDirSettings().toCommandLineArgs(computer)} should be used instead
    +     * @param computer Computer
    +     * @return Command line options for launching with the WorkDir
    +     */
    +    @Nonnull
    +    @Restricted(NoExternalUse.class)
    +    public String getWorkDirOptions(@Nonnull Computer computer) {
    +        if(!(computer instanceof SlaveComputer)) {
    +            return "";
    +        }
    +        return workDirSettings.toCommandLineString((SlaveComputer)computer);
    +    }
    +    
         @Extension @Symbol("jnlp")
         public static class DescriptorImpl extends Descriptor<ComputerLauncher> {
             public DescriptorImpl() {
    @@ -95,7 +179,23 @@ public class JNLPLauncher extends ComputerLauncher {
             public String getDisplayName() {
                 return Messages.JNLPLauncher_displayName();
             }
    -    };
    +        
    +        /**
    +         * Checks if Work Dir settings should be displayed.
    +         * 
    +         * This flag is checked in {@code config.jelly} before displaying the 
    +         * {@link JNLPLauncher#workDirSettings} property.
    +         * By default the configuration is displayed only for {@link JNLPLauncher},
    +         * but the implementation can be overridden.
    +         * @return {@code true} if work directories are supported by the launcher type.
    +         * @since 2.73
    +         */
    +        public boolean isWorkDirSupported() {
    +            // This property is included only for JNLPLauncher by default. 
    +            // Causes JENKINS-45895 in the case of includes otherwise
    +            return DescriptorImpl.class.equals(getClass());
    +        }
    +    }
     
         /**
          * Hides the JNLP launcher when the JNLP agent port is not enabled.
    @@ -110,7 +210,7 @@ public class JNLPLauncher extends ComputerLauncher {
              */
             @Override
             public boolean filter(@CheckForNull Object context, @Nonnull Descriptor descriptor) {
    -            return descriptor.clazz != JNLPLauncher.class || Jenkins.getInstance().getTcpSlaveAgentListener() != null;
    +            return descriptor.clazz != JNLPLauncher.class || Jenkins.get().getTcpSlaveAgentListener() != null;
             }
     
             /**
    @@ -118,8 +218,23 @@ public class JNLPLauncher extends ComputerLauncher {
              */
             @Override
             public boolean filterType(@Nonnull Class<?> contextClass, @Nonnull Descriptor descriptor) {
    -            return descriptor.clazz != JNLPLauncher.class || Jenkins.getInstance().getTcpSlaveAgentListener() != null;
    +            return descriptor.clazz != JNLPLauncher.class || Jenkins.get().getTcpSlaveAgentListener() != null;
             }
         }
     
    +    /**
    +     * Returns true if Java Web Start button should be displayed.
    +     * Java Web Start is only supported when the Jenkins server is
    +     * running with Java 8.  Earlier Java versions are not supported by Jenkins.
    +     * Later Java versions do not support Java Web Start.
    +     *
    +     * This flag is checked in {@code config.jelly} before displaying the
    +     * Java Web Start button.
    +     * @return {@code true} if Java Web Start button should be displayed.
    +     * @since FIXME
    +     */
    +    @Restricted(NoExternalUse.class) // Jelly use
    +    public boolean isJavaWebStartSupported() {
    +        return JavaUtils.isRunningWithJava8OrBelow();
    +    }
     }
    diff --git a/core/src/main/java/hudson/slaves/NodeDescriptor.java b/core/src/main/java/hudson/slaves/NodeDescriptor.java
    index 6f06289c38d35390d9a9a099b66dabb63965eca2..a7c137598e7f114338c343bb6fb34401f5d3b32c 100644
    --- a/core/src/main/java/hudson/slaves/NodeDescriptor.java
    +++ b/core/src/main/java/hudson/slaves/NodeDescriptor.java
    @@ -50,8 +50,8 @@ import javax.servlet.ServletException;
      *
      * <h2>Views</h2>
      * <p>
    - * This object needs to have <tt>newInstanceDetail.jelly</tt> view, which shows up in
    - * <tt>http://server/hudson/computers/new</tt> page as an explanation of this job type.
    + * This object needs to have {@code newInstanceDetail.jelly} view, which shows up in
    + * {@code http://server/hudson/computers/new} page as an explanation of this job type.
      *
      * <h2>Other Implementation Notes</h2>
      *
    @@ -112,7 +112,7 @@ public abstract class NodeDescriptor extends Descriptor<Node> {
          * Returns all the registered {@link NodeDescriptor} descriptors.
          */
         public static DescriptorExtensionList<Node,NodeDescriptor> all() {
    -        return Jenkins.getInstance().<Node,NodeDescriptor>getDescriptorList(Node.class);
    +        return Jenkins.get().getDescriptorList(Node.class);
         }
     
         /**
    @@ -121,10 +121,10 @@ public abstract class NodeDescriptor extends Descriptor<Node> {
          *      Use {@link #all()} for read access, and {@link Extension} for registration.
          */
         @Deprecated
    -    public static final DescriptorList<Node> ALL = new DescriptorList<Node>(Node.class);
    +    public static final DescriptorList<Node> ALL = new DescriptorList<>(Node.class);
     
         public static List<NodeDescriptor> allInstantiable() {
    -        List<NodeDescriptor> r = new ArrayList<NodeDescriptor>();
    +        List<NodeDescriptor> r = new ArrayList<>();
             for (NodeDescriptor d : all())
                 if(d.isInstantiable())
                     r.add(d);
    diff --git a/core/src/main/java/hudson/slaves/NodeList.java b/core/src/main/java/hudson/slaves/NodeList.java
    index e77ef0908454e262b36f46040e8fe017f7396c45..87f7a82328b7d64b056491237e822ed37eaebfe2 100644
    --- a/core/src/main/java/hudson/slaves/NodeList.java
    +++ b/core/src/main/java/hudson/slaves/NodeList.java
    @@ -49,7 +49,7 @@ import javax.annotation.CheckForNull;
      */
     public final class NodeList extends ArrayList<Node> {
         
    -    private Map<String,Node> map = new HashMap<String, Node>(); 
    +    private Map<String,Node> map = new HashMap<>();
         
         public NodeList() {
         }
    diff --git a/core/src/main/java/hudson/slaves/NodeProperty.java b/core/src/main/java/hudson/slaves/NodeProperty.java
    index 4a8240d268a1e5123af449dca8a3e66840268d32..a78c6d0f8925cd2b18f53208a61dd7836fe3d4fc 100644
    --- a/core/src/main/java/hudson/slaves/NodeProperty.java
    +++ b/core/src/main/java/hudson/slaves/NodeProperty.java
    @@ -83,7 +83,7 @@ public abstract class NodeProperty<N extends Node> implements ReconfigurableDesc
         protected void setNode(N node) { this.node = node; }
     
         public NodePropertyDescriptor getDescriptor() {
    -        return (NodePropertyDescriptor) Jenkins.getInstance().getDescriptorOrDie(getClass());
    +        return (NodePropertyDescriptor) Jenkins.get().getDescriptorOrDie(getClass());
         }
     
         /**
    @@ -178,7 +178,7 @@ public abstract class NodeProperty<N extends Node> implements ReconfigurableDesc
          * Lists up all the registered {@link NodeDescriptor}s in the system.
          */
         public static DescriptorExtensionList<NodeProperty<?>,NodePropertyDescriptor> all() {
    -        return (DescriptorExtensionList) Jenkins.getInstance().getDescriptorList(NodeProperty.class);
    +        return (DescriptorExtensionList) Jenkins.get().getDescriptorList(NodeProperty.class);
         }
     
         /**
    diff --git a/core/src/main/java/hudson/slaves/NodePropertyDescriptor.java b/core/src/main/java/hudson/slaves/NodePropertyDescriptor.java
    index 823ddb6eec1bda203862f3fb12e78e77613997e4..c585f7171363e00ccd48d458eca2c1c8000b74ae 100644
    --- a/core/src/main/java/hudson/slaves/NodePropertyDescriptor.java
    +++ b/core/src/main/java/hudson/slaves/NodePropertyDescriptor.java
    @@ -55,6 +55,6 @@ public abstract class NodePropertyDescriptor extends PropertyDescriptor<NodeProp
             // preserve legacy behaviour, even if brain-dead stupid, where applying to Jenkins was the discriminator
             // note that it would be a mistake to assume Jenkins.getInstance().getClass() == Jenkins.class
             // the groovy code tested against app.class, so we replicate that exact logic.
    -        return isApplicable(Jenkins.getInstance().getClass());
    +        return isApplicable(Jenkins.get().getClass());
         }
     }
    diff --git a/core/src/main/java/hudson/slaves/NodeProvisioner.java b/core/src/main/java/hudson/slaves/NodeProvisioner.java
    index af141a048f74d8cf157d31133907b000460a6579..7d1f865f1ee37aba7d0ca74ff99372bc53293508 100644
    --- a/core/src/main/java/hudson/slaves/NodeProvisioner.java
    +++ b/core/src/main/java/hudson/slaves/NodeProvisioner.java
    @@ -38,6 +38,7 @@ import javax.annotation.Nonnull;
     import javax.annotation.concurrent.GuardedBy;
     import java.awt.Color;
     import java.util.Arrays;
    +import java.util.Collections;
     import java.util.concurrent.Future;
     import java.util.concurrent.ExecutionException;
     import java.util.List;
    @@ -127,7 +128,7 @@ public class NodeProvisioner {
         private final Label label;
     
         private final AtomicReference<List<PlannedNode>> pendingLaunches
    -            = new AtomicReference<List<PlannedNode>>(new ArrayList<PlannedNode>());
    +            = new AtomicReference<>(new ArrayList<>());
     
         private final Lock provisioningLock = new ReentrantLock();
     
    @@ -159,7 +160,7 @@ public class NodeProvisioner {
          * @since 1.401
          */
         public List<PlannedNode> getPendingLaunches() {
    -        return new ArrayList<PlannedNode>(pendingLaunches.get());
    +        return new ArrayList<>(pendingLaunches.get());
         }
     
         /**
    @@ -207,15 +208,14 @@ public class NodeProvisioner {
                 Queue.withLock(new Runnable() {
                     @Override
                     public void run() {
    -                    Jenkins jenkins = Jenkins.getInstance();
    +                    Jenkins jenkins = Jenkins.get();
                         // clean up the cancelled launch activity, then count the # of executors that we are about to
                         // bring up.
     
                         int plannedCapacitySnapshot = 0;
     
    -                    List<PlannedNode> snapPendingLaunches = new ArrayList<PlannedNode>(pendingLaunches.get());
    -                    for (Iterator<PlannedNode> itr = snapPendingLaunches.iterator(); itr.hasNext(); ) {
    -                        PlannedNode f = itr.next();
    +                    List<PlannedNode> snapPendingLaunches = new ArrayList<>(pendingLaunches.get());
    +                    for (PlannedNode f : snapPendingLaunches) {
                             if (f.future.isDone()) {
                                 try {
                                     Node node = null;
    @@ -263,7 +263,7 @@ public class NodeProvisioner {
                                 } finally {
                                     while (true) {
                                         List<PlannedNode> orig = pendingLaunches.get();
    -                                    List<PlannedNode> repl = new ArrayList<PlannedNode>(orig);
    +                                    List<PlannedNode> repl = new ArrayList<>(orig);
                                         // the contract for List.remove(o) is that the first element i where
                                         // (o==null ? get(i)==null : o.equals(get(i)))
                                         // is true will be removed from the list
    @@ -305,15 +305,15 @@ public class NodeProvisioner {
                                     new Object[]{queueLengthSnapshot, availableSnapshot});
                             provisioningState = null;
                         } else {
    -                        provisioningState = new StrategyState(snapshot, label, plannedCapacitySnapshot);;
    +                        provisioningState = new StrategyState(snapshot, label, plannedCapacitySnapshot);
                         }
                     }
                 });
     
                 if (provisioningState != null) {
    -                List<Strategy> strategies = Jenkins.getInstance().getExtensionList(Strategy.class);
    +                List<Strategy> strategies = Jenkins.get().getExtensionList(Strategy.class);
                     for (Strategy strategy : strategies.isEmpty()
    -                        ? Arrays.<Strategy>asList(new StandardStrategyImpl())
    +                        ? Collections.<Strategy>singletonList(new StandardStrategyImpl())
                             : strategies) {
                         LOGGER.log(Level.FINER, "Consulting {0} provisioning strategy with state {1}",
                                 new Object[]{strategy, provisioningState});
    @@ -580,7 +580,7 @@ public class NodeProvisioner {
                 }
                 while (!plannedNodes.isEmpty()) {
                     List<PlannedNode> orig = pendingLaunches.get();
    -                List<PlannedNode> repl = new ArrayList<PlannedNode>(orig);
    +                List<PlannedNode> repl = new ArrayList<>(orig);
                     repl.addAll(plannedNodes);
                     if (pendingLaunches.compareAndSet(orig, repl)) {
                         if (additionalPlannedCapacity > 0) {
    @@ -687,7 +687,7 @@ public class NodeProvisioner {
                                 });
     
                         CLOUD:
    -                    for (Cloud c : Jenkins.getInstance().clouds) {
    +                    for (Cloud c : Jenkins.get().clouds) {
                             if (excessWorkload < 0) {
                                 break;  // enough agents allocated
                             }
    @@ -803,9 +803,9 @@ public class NodeProvisioner {
     
             @Override
             protected void doRun() {
    -            Jenkins h = Jenkins.getInstance();
    -            h.unlabeledNodeProvisioner.update();
    -            for( Label l : h.getLabels() )
    +            Jenkins j = Jenkins.get();
    +            j.unlabeledNodeProvisioner.update();
    +            for( Label l : j.getLabels() )
                     l.nodeProvisioner.update();
             }
         }
    diff --git a/core/src/main/java/hudson/slaves/OfflineCause.java b/core/src/main/java/hudson/slaves/OfflineCause.java
    index 3d711dd5f0ccd710fd267d94172871879b88aa36..a158ce2b8c15da689b612fc22634561eae5bd181 100644
    --- a/core/src/main/java/hudson/slaves/OfflineCause.java
    +++ b/core/src/main/java/hudson/slaves/OfflineCause.java
    @@ -44,7 +44,7 @@ import java.util.Date;
      *
      * <h2>Views</h2>
      * <p>
    - * {@link OfflineCause} must have <tt>cause.jelly</tt> that renders a cause
    + * {@link OfflineCause} must have {@code cause.jelly} that renders a cause
      * into HTML. This is used to tell users why the node is put offline.
      * This view should render a block element like DIV.
      *
    @@ -103,7 +103,6 @@ public abstract class OfflineCause {
          * Caused by unexpected channel termination.
          */
         public static class ChannelTermination extends OfflineCause {
    -        @Exported
             public final Exception cause;
     
             public ChannelTermination(Exception cause) {
    diff --git a/core/src/main/java/hudson/slaves/RetentionStrategy.java b/core/src/main/java/hudson/slaves/RetentionStrategy.java
    index 3d8c79e544ad1f154e00a695e9edef3c9361dc54..c2237c3a56fe7f8282ce5e016b6eb0530f1051cd 100644
    --- a/core/src/main/java/hudson/slaves/RetentionStrategy.java
    +++ b/core/src/main/java/hudson/slaves/RetentionStrategy.java
    @@ -39,6 +39,7 @@ import javax.annotation.concurrent.GuardedBy;
     import java.util.concurrent.TimeUnit;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +import javax.annotation.Nonnull;
     
     /**
      * Controls when to take {@link Computer} offline, bring it back online, or even to destroy it.
    @@ -57,7 +58,7 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
          *         rechecked earlier or later that this!
          */
         @GuardedBy("hudson.model.Queue.lock")
    -    public abstract long check(T c);
    +    public abstract long check(@Nonnull T c);
     
         /**
          * This method is called to determine whether manual launching of the agent is allowed at this point in time.
    @@ -92,22 +93,18 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
          * The default implementation of this method delegates to {@link #check(Computer)},
          * but this allows {@link RetentionStrategy} to distinguish the first time invocation from the rest.
          *
    +     * @param c Computer instance
          * @since 1.275
          */
    -    public void start(final T c) {
    -        Queue.withLock(new Runnable() {
    -            @Override
    -            public void run() {
    -                check(c);
    -            }
    -        });
    +    public void start(final @Nonnull T c) {
    +        Queue.withLock((Runnable) () -> check(c));
         }
     
         /**
          * Returns all the registered {@link RetentionStrategy} descriptors.
          */
         public static DescriptorExtensionList<RetentionStrategy<?>,Descriptor<RetentionStrategy<?>>> all() {
    -        return (DescriptorExtensionList) Jenkins.getInstance().getDescriptorList(RetentionStrategy.class);
    +        return (DescriptorExtensionList) Jenkins.get().getDescriptorList(RetentionStrategy.class);
         }
     
         /**
    @@ -121,26 +118,27 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
         /**
          * Dummy instance that doesn't do any attempt to retention.
          */
    -    public static final RetentionStrategy<Computer> NOOP = new RetentionStrategy<Computer>() {
    +    public static final RetentionStrategy<Computer> NOOP = new NoOp();
    +    private static final class NoOp extends RetentionStrategy<Computer> {
             @GuardedBy("hudson.model.Queue.lock")
    +        @Override
             public long check(Computer c) {
                 return 60;
             }
    -
             @Override
             public void start(Computer c) {
                 c.connect(false);
             }
    -
             @Override
             public Descriptor<RetentionStrategy<?>> getDescriptor() {
                 return DESCRIPTOR;
             }
    -
    -        private final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
    -
    -        class DescriptorImpl extends Descriptor<RetentionStrategy<?>> {}
    -    };
    +        private Object readResolve() {
    +            return NOOP;
    +        }
    +        private static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
    +        private static final class DescriptorImpl extends Descriptor<RetentionStrategy<?>> {}
    +    }
     
         /**
          * Convenient singleton instance, since this {@link RetentionStrategy} is stateless.
    @@ -218,8 +216,8 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
             @GuardedBy("hudson.model.Queue.lock")
             public long check(final SlaveComputer c) {
                 if (c.isOffline() && c.isLaunchSupported()) {
    -                final HashMap<Computer, Integer> availableComputers = new HashMap<Computer, Integer>();
    -                for (Computer o : Jenkins.getInstance().getComputers()) {
    +                final HashMap<Computer, Integer> availableComputers = new HashMap<>();
    +                for (Computer o : Jenkins.get().getComputers()) {
                         if ((o.isOnline() || o.isConnecting()) && o.isPartiallyIdle() && o.isAcceptingTasks()) {
                             final int idleExecutors = o.countIdle();
                             if (idleExecutors>0)
    diff --git a/core/src/main/java/hudson/slaves/SlaveComputer.java b/core/src/main/java/hudson/slaves/SlaveComputer.java
    index a1ed2332781f2b5317d749dc9b997edb28153efa..5020cff41bdbe950452d15089b6609c7171be798 100644
    --- a/core/src/main/java/hudson/slaves/SlaveComputer.java
    +++ b/core/src/main/java/hudson/slaves/SlaveComputer.java
    @@ -26,6 +26,7 @@ package hudson.slaves;
     import hudson.AbortException;
     import hudson.FilePath;
     import hudson.Functions;
    +import hudson.RestrictedSince;
     import hudson.Util;
     import hudson.console.ConsoleLogFilter;
     import hudson.model.Computer;
    @@ -38,15 +39,17 @@ import hudson.model.TaskListener;
     import hudson.model.User;
     import hudson.remoting.Channel;
     import hudson.remoting.ChannelBuilder;
    +import hudson.remoting.ChannelClosedException;
    +import hudson.remoting.CommandTransport;
     import hudson.remoting.Launcher;
     import hudson.remoting.VirtualChannel;
     import hudson.security.ACL;
     import hudson.slaves.OfflineCause.ChannelTermination;
     import hudson.util.Futures;
    -import hudson.util.IOUtils;
     import hudson.util.NullStream;
     import hudson.util.RingBufferLogHandler;
     import hudson.util.StreamTaskListener;
    +import hudson.util.VersionNumber;
     import hudson.util.io.RewindableFileOutputStream;
     import hudson.util.io.RewindableRotatingFileOutputStream;
     import jenkins.model.Jenkins;
    @@ -54,21 +57,26 @@ import jenkins.security.ChannelConfigurator;
     import jenkins.security.MasterToSlaveCallable;
     import jenkins.slaves.EncryptedSlaveAgentJnlpFile;
     import jenkins.slaves.JnlpSlaveAgentProtocol;
    +import jenkins.slaves.RemotingVersionInfo;
     import jenkins.slaves.systemInfo.SlaveSystemInfo;
     import jenkins.util.SystemProperties;
     import org.acegisecurity.context.SecurityContext;
     import org.acegisecurity.context.SecurityContextHolder;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.Beta;
    +import org.kohsuke.accmod.restrictions.DoNotUse;
     import org.kohsuke.stapler.HttpRedirect;
     import org.kohsuke.stapler.HttpResponse;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     import org.kohsuke.stapler.WebMethod;
    +import org.kohsuke.stapler.export.Exported;
     import org.kohsuke.stapler.interceptor.RequirePOST;
     
     import javax.annotation.CheckForNull;
    +import javax.annotation.Nonnull;
     import javax.annotation.OverridingMethodsMustInvokeSuper;
    -import javax.servlet.ServletException;
     import java.io.File;
     import java.io.IOException;
     import java.io.InputStream;
    @@ -86,6 +94,7 @@ import java.util.logging.LogRecord;
     import java.util.logging.Logger;
     
     import static hudson.slaves.SlaveComputer.LogHolder.SLAVE_LOG_HANDLER;
    +import org.jenkinsci.remoting.util.LoggingChannelListener;
     
     
     /**
    @@ -234,10 +243,33 @@ public class SlaveComputer extends Computer {
             return launcher.isLaunchSupported();
         }
     
    +    /**
    +     * Return the {@code ComputerLauncher} for this SlaveComputer.
    +     * @since 1.312
    +     */
         public ComputerLauncher getLauncher() {
             return launcher;
         }
     
    +    /**
    +     * Return the {@code ComputerLauncher} for this SlaveComputer, strips off
    +     * any {@code DelegatingComputerLauncher}s or {@code ComputerLauncherFilter}s.
    +     * @since 2.83
    +     */
    +    public ComputerLauncher getDelegatedLauncher() {
    +        ComputerLauncher l = launcher;
    +        while (true) {
    +            if (l instanceof DelegatingComputerLauncher) {
    +                l = ((DelegatingComputerLauncher) l).getLauncher();
    +            } else if (l instanceof ComputerLauncherFilter) {
    +                l = ((ComputerLauncherFilter) l).getCore();
    +            } else {
    +                break;
    +            }
    +        }
    +        return l;
    +    }
    +
         protected Future<?> _connect(boolean forceReconnect) {
             if(channel!=null)   return Futures.precomputed(null);
             if(!forceReconnect && isConnecting())
    @@ -354,7 +386,15 @@ public class SlaveComputer extends Computer {
     
         private final Object channelLock = new Object();
     
    -    public void setChannel(InputStream in, OutputStream out, TaskListener taskListener, Channel.Listener listener) throws IOException, InterruptedException {
    +    /**
    +     * Creates a {@link Channel} from the given stream and sets that to this agent.
    +     *
    +     * Same as {@link #setChannel(InputStream, OutputStream, OutputStream, Channel.Listener)}, but for
    +     * {@link TaskListener}.
    +     */
    +    public void setChannel(@Nonnull InputStream in, @Nonnull OutputStream out,
    +                           @Nonnull TaskListener taskListener,
    +                           @CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
             setChannel(in,out,taskListener.getLogger(),listener);
         }
     
    @@ -362,13 +402,13 @@ public class SlaveComputer extends Computer {
          * Creates a {@link Channel} from the given stream and sets that to this agent.
          *
          * @param in
    -     *      Stream connected to the remote "slave.jar". It's the caller's responsibility to do
    +     *      Stream connected to the remote agent. It's the caller's responsibility to do
          *      buffering on this stream, if that's necessary.
          * @param out
          *      Stream connected to the remote peer. It's the caller's responsibility to do
          *      buffering on this stream, if that's necessary.
          * @param launchLog
    -     *      If non-null, receive the portion of data in <tt>is</tt> before
    +     *      If non-null, receive the portion of data in {@code is} before
          *      the data goes into the "binary mode". This is useful
          *      when the established communication channel might include some data that might
          *      be useful for debugging/trouble-shooting.
    @@ -377,7 +417,9 @@ public class SlaveComputer extends Computer {
          *      By the time this method is called, the cause of the termination is reported to the user,
          *      so the implementation of the listener doesn't need to do that again.
          */
    -    public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException {
    +    public void setChannel(@Nonnull InputStream in, @Nonnull OutputStream out,
    +                           @CheckForNull OutputStream launchLog,
    +                           @CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
             ChannelBuilder cb = new ChannelBuilder(nodeName,threadPoolForRemoting)
                 .withMode(Channel.Mode.NEGOTIATE)
                 .withHeaderStream(launchLog);
    @@ -390,6 +432,39 @@ public class SlaveComputer extends Computer {
             setChannel(channel,launchLog,listener);
         }
     
    +    /**
    +     * Creates a {@link Channel} from the given Channel Builder and Command Transport.
    +     * This method can be used to allow {@link ComputerLauncher}s to create channels not based on I/O streams.
    +     *
    +     * @param cb
    +     *      Channel Builder.
    +     *      To print launch logs this channel builder should have a Header Stream defined
    +     *      (see {@link ChannelBuilder#getHeaderStream()}) in this argument or by one of {@link ChannelConfigurator}s.
    +     * @param commandTransport
    +     *      Command Transport
    +     * @param listener
    +     *      Gets a notification when the channel closes, to perform clean up. Can be {@code null}.
    +     *      By the time this method is called, the cause of the termination is reported to the user,
    +     *      so the implementation of the listener doesn't need to do that again.
    +     * @since 2.127
    +     */
    +    @Restricted(Beta.class)
    +    public void setChannel(@Nonnull ChannelBuilder cb,
    +                           @Nonnull CommandTransport commandTransport,
    +                           @CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
    +        for (ChannelConfigurator cc : ChannelConfigurator.all()) {
    +            cc.onChannelBuilding(cb,this);
    +        }
    +
    +        OutputStream headerStream = cb.getHeaderStream();
    +        if (headerStream == null) {
    +            LOGGER.log(Level.WARNING, "No header stream defined when setting channel for computer {0}. " +
    +                    "Launch log won't be printed", this);
    +        }
    +        Channel channel = cb.build(commandTransport);
    +        setChannel(channel, headerStream, listener);
    +    }
    +
         /**
          * Shows {@link Channel#classLoadingCount}.
          * @since 1.495
    @@ -447,6 +522,26 @@ public class SlaveComputer extends Computer {
             return channel == null ? null : absoluteRemoteFs;
         }
     
    +    /**
    +     * Just for restFul api.
    +     * Returns the remote FS root absolute path or {@code null} if the agent is off-line. The absolute path may change
    +     * between connections if the connection method does not provide a consistent working directory and the node's
    +     * remote FS is specified as a relative path.
    +     * @see #getAbsoluteRemoteFs()
    +     * @return the remote FS root absolute path or {@code null} if the agent is off-line or don't have connect permission.
    +     * @since 2.125
    +     */
    +    @Exported
    +    @Restricted(DoNotUse.class)
    +    @CheckForNull
    +    public String getAbsoluteRemotePath() {
    +        if(hasPermission(CONNECT)) {
    +            return getAbsoluteRemoteFs();
    +        } else {
    +            return null;
    +        }
    +    }
    +
         static class LoadingCount extends MasterToSlaveCallable<Integer,RuntimeException> {
             private final boolean resource;
             LoadingCount(boolean resource) {
    @@ -454,13 +549,20 @@ public class SlaveComputer extends Computer {
             }
             @Override public Integer call() {
                 Channel c = Channel.current();
    +            if (c == null) {
    +                return -1;
    +            }
                 return resource ? c.resourceLoadingCount.get() : c.classLoadingCount.get();
             }
         }
     
         static class LoadingPrefetchCacheCount extends MasterToSlaveCallable<Integer,RuntimeException> {
             @Override public Integer call() {
    -            return Channel.current().classLoadingPrefetchCacheCount.get();
    +            Channel c = Channel.current();
    +            if (c == null) {
    +                return -1;
    +            }
    +            return c.classLoadingPrefetchCacheCount.get();
             }
         }
     
    @@ -471,25 +573,32 @@ public class SlaveComputer extends Computer {
             }
             @Override public Long call() {
                 Channel c = Channel.current();
    +            if (c == null) {
    +                return -1L;
    +            }
                 return resource ? c.resourceLoadingTime.get() : c.classLoadingTime.get();
             }
         }
     
         /**
          * Sets up the connection through an existing channel.
    -     * @param channel the channel to use; <strong>warning:</strong> callers are expected to have called {@link ChannelConfigurator} already
    +     * @param channel the channel to use; <strong>warning:</strong> callers are expected to have called {@link ChannelConfigurator} already.
    +     * @param launchLog Launch log. If not {@code null}, will receive launch log messages
    +     * @param listener Channel event listener to be attached (if not {@code null})
          * @since 1.444
          */
    -    public void setChannel(Channel channel, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException {
    +    public void setChannel(@Nonnull Channel channel,
    +                           @CheckForNull OutputStream launchLog,
    +                           @CheckForNull Channel.Listener listener) throws IOException, InterruptedException {
             if(this.channel!=null)
                 throw new IllegalStateException("Already connected");
     
    -        final TaskListener taskListener = new StreamTaskListener(launchLog);
    +        final TaskListener taskListener = launchLog != null ? new StreamTaskListener(launchLog) : TaskListener.NULL;
             PrintStream log = taskListener.getLogger();
     
             channel.setProperty(SlaveComputer.class, this);
     
    -        channel.addListener(new Channel.Listener() {
    +        channel.addListener(new LoggingChannelListener(logger, Level.FINEST) {
                 @Override
                 public void onClosed(Channel c, IOException cause) {
                     // Orderly shutdown will have null exception
    @@ -515,7 +624,13 @@ public class SlaveComputer extends Computer {
                 channel.addListener(listener);
     
             String slaveVersion = channel.call(new SlaveVersion());
    -        log.println("Slave.jar version: " + slaveVersion);
    +        log.println("Remoting version: " + slaveVersion);
    +        VersionNumber agentVersion = new VersionNumber(slaveVersion);
    +        if (agentVersion.isOlderThan(RemotingVersionInfo.getMinimumSupportedVersion())) {
    +            log.println(String.format("WARNING: Remoting version is older than a minimum required one (%s). " +
    +                    "Connection will not be rejected, but the compatibility is NOT guaranteed",
    +                    RemotingVersionInfo.getMinimumSupportedVersion()));
    +        }
     
             boolean _isUnix = channel.call(new DetectOS());
             log.println(_isUnix? hudson.model.Messages.Slave_UnixSlave():hudson.model.Messages.Slave_WindowsSlave());
    @@ -585,7 +700,7 @@ public class SlaveComputer extends Computer {
                 SecurityContextHolder.setContext(old);
             }
             log.println("Agent successfully connected and online");
    -        Jenkins.getInstance().getQueue().scheduleMaintenance();
    +        Jenkins.get().getQueue().scheduleMaintenance();
         }
     
         @Override
    @@ -605,7 +720,7 @@ public class SlaveComputer extends Computer {
         }
     
         @RequirePOST
    -    public HttpResponse doDoDisconnect(@QueryParameter String offlineMessage) throws IOException, ServletException {
    +    public HttpResponse doDoDisconnect(@QueryParameter String offlineMessage) {
             if (channel!=null) {
                 //does nothing in case computer is already disconnected
                 checkPermission(DISCONNECT);
    @@ -630,9 +745,18 @@ public class SlaveComputer extends Computer {
         }
     
         @RequirePOST
    -    public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    +    @Override
    +    public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException {
    +        checkPermission(CONNECT);
    +            
             if(channel!=null) {
    -            req.getView(this,"already-launched.jelly").forward(req, rsp);
    +            try {
    +                req.getView(this, "already-launched.jelly").forward(req, rsp);
    +            } catch (IOException x) {
    +                throw x;
    +            } catch (/*Servlet*/Exception x) {
    +                throw new IOException(x);
    +            }
                 return;
             }
     
    @@ -665,7 +789,7 @@ public class SlaveComputer extends Computer {
         }
     
         @WebMethod(name="slave-agent.jnlp")
    -    public HttpResponse doSlaveAgentJnlp(StaplerRequest req, StaplerResponse res) throws IOException, ServletException {
    +    public HttpResponse doSlaveAgentJnlp(StaplerRequest req, StaplerResponse res) {
             return new EncryptedSlaveAgentJnlpFile(this, "slave-agent.jnlp.jelly", getName(), CONNECT);
         }
     
    @@ -688,7 +812,7 @@ public class SlaveComputer extends Computer {
     
         public RetentionStrategy getRetentionStrategy() {
             Slave n = getNode();
    -        return n==null ? RetentionStrategy.INSTANCE : n.getRetentionStrategy();
    +        return n==null ? RetentionStrategy.NOOP : n.getRetentionStrategy();
         }
     
         /**
    @@ -822,7 +946,7 @@ public class SlaveComputer extends Computer {
             public Void call() {
                 SLAVE_LOG_HANDLER = new RingBufferLogHandler(ringBufferSize);
     
    -            // avoid double installation of the handler. JNLP slaves can reconnect to the master multiple times
    +            // avoid double installation of the handler. JNLP agents can reconnect to the master multiple times
                 // and each connection gets a different RemoteClassLoader, so we need to evict them by class name,
                 // not by their identity.
                 for (Handler h : LOGGER.getHandlers()) {
    @@ -838,7 +962,11 @@ public class SlaveComputer extends Computer {
                     // ignore this error.
                 }
     
    -            Channel.current().setProperty("slave",Boolean.TRUE); // indicate that this side of the channel is the slave side.
    +            try {
    +                getChannelOrFail().setProperty("slave",Boolean.TRUE); // indicate that this side of the channel is the agent side.
    +            } catch (ChannelClosedException e) {
    +                throw new IllegalStateException(e);
    +            }
     
                 return null;
             }
    @@ -870,13 +998,15 @@ public class SlaveComputer extends Computer {
         /**
          * Helper method for Jelly.
          */
    +    @Restricted(DoNotUse.class)
    +    @RestrictedSince("TODO")
         public static List<SlaveSystemInfo> getSystemInfoExtensions() {
             return SlaveSystemInfo.all();
         }
     
         private static class SlaveLogFetcher extends MasterToSlaveCallable<List<LogRecord>,RuntimeException> {
             public List<LogRecord> call() {
    -            return new ArrayList<LogRecord>(SLAVE_LOG_HANDLER.getView());
    +            return new ArrayList<>(SLAVE_LOG_HANDLER.getView());
             }
         }
     
    diff --git a/core/src/main/java/hudson/slaves/WorkspaceList.java b/core/src/main/java/hudson/slaves/WorkspaceList.java
    index 17b789d6bc75fe4dbf2ead513c045ea90cefb051..97283fd6d7aed1e88ffd698299fb3bffbdcd892c 100644
    --- a/core/src/main/java/hudson/slaves/WorkspaceList.java
    +++ b/core/src/main/java/hudson/slaves/WorkspaceList.java
    @@ -112,7 +112,9 @@ public final class WorkspaceList {
             public final @Nonnull FilePath path;
     
             protected Lease(@Nonnull FilePath path) {
    -            path.getRemote(); // null check
    +            if (path == null) { // Protection from old API
    +                throw new NullPointerException("The specified FilePath is null");
    +            }
                 this.path = path;
             }
     
    @@ -153,7 +155,7 @@ public final class WorkspaceList {
             }
         }
     
    -    private final Map<FilePath,Entry> inUse = new HashMap<FilePath,Entry>();
    +    private final Map<FilePath,Entry> inUse = new HashMap<>();
     
         public WorkspaceList() {
         }
    diff --git a/core/src/main/java/hudson/tasks/ArtifactArchiver.java b/core/src/main/java/hudson/tasks/ArtifactArchiver.java
    index d541c8c2a87db4bbd45a34b69c10de1285121a58..b90fe33a3ab6d5f9a7ece2980c645b5e9dea181d 100644
    --- a/core/src/main/java/hudson/tasks/ArtifactArchiver.java
    +++ b/core/src/main/java/hudson/tasks/ArtifactArchiver.java
    @@ -24,6 +24,7 @@
     package hudson.tasks;
     
     import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    +import hudson.AbortException;
     import hudson.FilePath;
     import jenkins.MasterToSlaveFileCallable;
     import hudson.Launcher;
    @@ -140,9 +141,9 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep {
         }
     
         // Backwards compatibility for older builds
    -    @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", 
    +    @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE",
                 justification = "Null checks in readResolve are valid since we deserialize and upgrade objects")
    -    public Object readResolve() {
    +    protected Object readResolve() {
             if (allowEmptyArchive == null) {
                 this.allowEmptyArchive = SystemProperties.getBoolean(ArtifactArchiver.class.getName()+".warnOnEmpty");
             }
    @@ -213,20 +214,10 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep {
             this.caseSensitive = caseSensitive;
         }
     
    -    private void listenerWarnOrError(TaskListener listener, String message) {
    -    	if (allowEmptyArchive) {
    -    		listener.getLogger().println(String.format("WARN: %s", message));
    -    	} else {
    -    		listener.error(message);
    -    	}
    -    }
    -
         @Override
    -    public void perform(Run<?,?> build, FilePath ws, Launcher launcher, TaskListener listener) throws InterruptedException {
    +    public void perform(Run<?,?> build, FilePath ws, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
             if(artifacts.length()==0) {
    -            listener.error(Messages.ArtifactArchiver_NoIncludes());
    -            build.setResult(Result.FAILURE);
    -            return;
    +            throw new AbortException(Messages.ArtifactArchiver_NoIncludes());
             }
     
             Result result = build.getResult();
    @@ -248,28 +239,29 @@ public class ArtifactArchiver extends Recorder implements SimpleBuildStep {
                 } else {
                     result = build.getResult();
                     if (result == null || result.isBetterOrEqualTo(Result.UNSTABLE)) {
    -                    // If the build failed, don't complain that there was no matching artifact.
    -                    // The build probably didn't even get to the point where it produces artifacts. 
    -                    listenerWarnOrError(listener, Messages.ArtifactArchiver_NoMatchFound(artifacts));
    -                    String msg = null;
                         try {
    -                    	msg = ws.validateAntFileMask(artifacts, FilePath.VALIDATE_ANT_FILE_MASK_BOUND, caseSensitive);
    +                    	String msg = ws.validateAntFileMask(artifacts, FilePath.VALIDATE_ANT_FILE_MASK_BOUND, caseSensitive);
    +                        if (msg != null) {
    +                            listener.getLogger().println(msg);
    +                        }
                         } catch (Exception e) {
    -                    	listenerWarnOrError(listener, e.getMessage());
    +                        Functions.printStackTrace(e, listener.getLogger());
                         }
    -                    if(msg!=null)
    -                        listenerWarnOrError(listener, msg);
    -                }
    -                if (!allowEmptyArchive) {
    -                	build.setResult(Result.FAILURE);
    +                    if (allowEmptyArchive) {
    +                        listener.getLogger().println(Messages.ArtifactArchiver_NoMatchFound(artifacts));
    +                    } else {
    +                        throw new AbortException(Messages.ArtifactArchiver_NoMatchFound(artifacts));
    +                    }
    +                } else {
    +                    // If a freestyle build failed, do not complain that there was no matching artifact:
    +                    // the build probably did not even get to the point where it produces artifacts.
    +                    // For Pipeline, the program ought not be *trying* to archive anything after a failure,
    +                    // but anyway most likely result == null above so we would not be here.
                     }
    -                return;
                 }
    -        } catch (IOException e) {
    -            Util.displayIOException(e,listener);
    -            Functions.printStackTrace(e, listener.error(Messages.ArtifactArchiver_FailedToArchive(artifacts)));
    -            build.setResult(Result.FAILURE);
    -            return;
    +        } catch (java.nio.file.AccessDeniedException e) {
    +            LOG.log(Level.FINE, "Diagnosing anticipated Exception", e);
    +            throw new AbortException(e.toString()); // Message is not enough as that is the filename only
             }
         }
     
    diff --git a/core/src/main/java/hudson/tasks/BuildStep.java b/core/src/main/java/hudson/tasks/BuildStep.java
    index 16f0c0268cc7223ba33c748a416a6d6070473adb..ad81455335c1db7049e11347484230207b4988ac 100644
    --- a/core/src/main/java/hudson/tasks/BuildStep.java
    +++ b/core/src/main/java/hudson/tasks/BuildStep.java
    @@ -65,7 +65,7 @@ import jenkins.model.Jenkins;
      * So generally speaking, derived classes should use instance variables
      * only for keeping configuration. You can still store objects you use
      * for processing, like a parser of some sort, but they need to be marked
    - * as <tt>transient</tt>, and the code needs to be aware that they might
    + * as {@code transient}, and the code needs to be aware that they might
      * be null (which is the case when you access the field for the first time
      * the object is restored.)
      *
    @@ -145,7 +145,7 @@ public interface BuildStep {
          * it owns when the rendering is requested.
          *
          * <p>
    -     * This action can have optional <tt>jobMain.jelly</tt> view, which will be
    +     * This action can have optional {@code jobMain.jelly} view, which will be
          * aggregated into the main panel of the job top page. The jelly file
          * should have an {@code <h2>} tag that shows the section title, followed by some
          * block elements to render the details of the section.
    diff --git a/core/src/main/java/hudson/tasks/BuildTrigger.java b/core/src/main/java/hudson/tasks/BuildTrigger.java
    index 119d5794afe752677530c2c401a8c6da748fc554..78f8ef13f8a10df1597782725e84af532db3e4df 100644
    --- a/core/src/main/java/hudson/tasks/BuildTrigger.java
    +++ b/core/src/main/java/hudson/tasks/BuildTrigger.java
    @@ -410,7 +410,7 @@ public class BuildTrigger extends Recorder implements DependencyDeclarer {
                             return FormValidation.error(Messages.BuildTrigger_NotBuildable(projectName));
                         // check whether the supposed user is expected to be able to build
                         Authentication auth = Tasks.getAuthenticationOf(project);
    -                    if (!item.getACL().hasPermission(auth, Item.BUILD)) {
    +                    if (!item.hasPermission(auth, Item.BUILD)) {
                             return FormValidation.error(Messages.BuildTrigger_you_have_no_permission_to_build_(projectName));
                         }
                         hasProjects = true;
    @@ -431,7 +431,7 @@ public class BuildTrigger extends Recorder implements DependencyDeclarer {
             public static class ItemListenerImpl extends ItemListener {
                 @Override
                 public void onLocationChanged(final Item item, final String oldFullName, final String newFullName) {
    -                try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
    +                try (ACLContext acl = ACL.as(ACL.SYSTEM)) {
                         locationChanged(item, oldFullName, newFullName);
                     }
                 }
    diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java
    index fb45ef9cc2cba11f4147b863d92b9c29b1081909..c563180e0130d1c4060d335942979db68779ff25 100644
    --- a/core/src/main/java/hudson/tasks/Fingerprinter.java
    +++ b/core/src/main/java/hudson/tasks/Fingerprinter.java
    @@ -188,59 +188,69 @@ public class Fingerprinter extends Recorder implements Serializable, DependencyD
             }
         }
     
    -    private void record(Run<?,?> build, FilePath ws, TaskListener listener, Map<String,String> record, final String targets) throws IOException, InterruptedException {
    -        final class Record implements Serializable {
    -            final boolean produced;
    -            final String relativePath;
    -            final String fileName;
    -            final String md5sum;
    -
    -            public Record(boolean produced, String relativePath, String fileName, String md5sum) {
    -                this.produced = produced;
    -                this.relativePath = relativePath;
    -                this.fileName = fileName;
    -                this.md5sum = md5sum;
    -            }
    -
    -            Fingerprint addRecord(Run build) throws IOException {
    -                FingerprintMap map = Jenkins.getInstance().getFingerprintMap();
    -                return map.getOrCreate(produced?build:null, fileName, md5sum);
    -            }
    +    private static final class Record implements Serializable {
    +
    +        final boolean produced;
    +        final String relativePath;
    +        final String fileName;
    +        final String md5sum;
    +
    +        public Record(boolean produced, String relativePath, String fileName, String md5sum) {
    +            this.produced = produced;
    +            this.relativePath = relativePath;
    +            this.fileName = fileName;
    +            this.md5sum = md5sum;
    +        }
     
    -            private static final long serialVersionUID = 1L;
    +        Fingerprint addRecord(Run build) throws IOException {
    +            FingerprintMap map = Jenkins.getInstance().getFingerprintMap();
    +            return map.getOrCreate(produced?build:null, fileName, md5sum);
             }
     
    -        final long buildTimestamp = build.getTimeInMillis();
    +        private static final long serialVersionUID = 1L;
    +    }
     
    -        List<Record> records = ws.act(new MasterToSlaveFileCallable<List<Record>>() {
    -            public List<Record> invoke(File baseDir, VirtualChannel channel) throws IOException {
    -                List<Record> results = new ArrayList<Record>();
    +    private static final class FindRecords extends MasterToSlaveFileCallable<List<Record>> {
     
    -                FileSet src = Util.createFileSet(baseDir,targets);
    +        private final String targets;
    +        private final long buildTimestamp;
     
    -                DirectoryScanner ds = src.getDirectoryScanner();
    -                for( String f : ds.getIncludedFiles() ) {
    -                    File file = new File(baseDir,f);
    +        FindRecords(String targets, long buildTimestamp) {
    +            this.targets = targets;
    +            this.buildTimestamp = buildTimestamp;
    +        }
     
    -                    // consider the file to be produced by this build only if the timestamp
    -                    // is newer than when the build has started.
    -                    // 2000ms is an error margin since since VFAT only retains timestamp at 2sec precision
    -                    boolean produced = buildTimestamp <= file.lastModified()+2000;
    +        @Override
    +        public List<Record> invoke(File baseDir, VirtualChannel channel) throws IOException {
    +            List<Record> results = new ArrayList<Record>();
     
    -                    try {
    -                        results.add(new Record(produced,f,file.getName(),new FilePath(file).digest()));
    -                    } catch (IOException e) {
    -                        throw new IOException(Messages.Fingerprinter_DigestFailed(file),e);
    -                    } catch (InterruptedException e) {
    -                        throw new IOException(Messages.Fingerprinter_Aborted(),e);
    -                    }
    -                }
    +            FileSet src = Util.createFileSet(baseDir,targets);
     
    -                return results;
    +            DirectoryScanner ds = src.getDirectoryScanner();
    +            for( String f : ds.getIncludedFiles() ) {
    +                File file = new File(baseDir,f);
    +
    +                // consider the file to be produced by this build only if the timestamp
    +                // is newer than when the build has started.
    +                // 2000ms is an error margin since since VFAT only retains timestamp at 2sec precision
    +                boolean produced = buildTimestamp <= file.lastModified()+2000;
    +
    +                try {
    +                    results.add(new Record(produced,f,file.getName(),new FilePath(file).digest()));
    +                } catch (IOException e) {
    +                    throw new IOException(Messages.Fingerprinter_DigestFailed(file),e);
    +                } catch (InterruptedException e) {
    +                    throw new IOException(Messages.Fingerprinter_Aborted(),e);
    +                }
                 }
    -        });
     
    -        for (Record r : records) {
    +            return results;
    +        }
    +
    +    }
    +
    +    private void record(Run<?,?> build, FilePath ws, TaskListener listener, Map<String,String> record, final String targets) throws IOException, InterruptedException {
    +        for (Record r : ws.act(new FindRecords(targets, build.getTimeInMillis()))) {
                 Fingerprint fp = r.addRecord(build);
                 if(fp==null) {
                     listener.error(Messages.Fingerprinter_FailedFor(r.relativePath));
    diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java
    index 61354ed1c1e854cac86603f55192c9fd6be911a8..5bccbfe78548651ed390c081f0e986bf3c53249a 100644
    --- a/core/src/main/java/hudson/tasks/Maven.java
    +++ b/core/src/main/java/hudson/tasks/Maven.java
    @@ -24,6 +24,7 @@
     package hudson.tasks;
     
     import hudson.Extension;
    +import hudson.model.PersistentDescriptor;
     import jenkins.MasterToSlaveFileCallable;
     import hudson.Launcher;
     import hudson.Functions;
    @@ -245,7 +246,7 @@ public class Maven extends Builder {
         }
     
         /**
    -     * Looks for <tt>pom.xlm</tt> or <tt>project.xml</tt> to determine the maven executable
    +     * Looks for {@code pom.xlm} or {@code project.xml} to determine the maven executable
          * name.
          */
         private static final class DecideDefaultMavenCommand extends MasterToSlaveFileCallable<String> {
    @@ -424,13 +425,12 @@ public class Maven extends Builder {
         public static DescriptorImpl DESCRIPTOR;
     
         @Extension @Symbol("maven")
    -    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
    +    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> implements PersistentDescriptor {
             @CopyOnWrite
             private volatile MavenInstallation[] installations = new MavenInstallation[0];
     
             public DescriptorImpl() {
                 DESCRIPTOR = this;
    -            load();
             }
     
             public boolean isApplicable(Class<? extends AbstractProject> jobType) {
    @@ -553,29 +553,7 @@ public class Maven extends Builder {
             public boolean meetsMavenReqVersion(Launcher launcher, int mavenReqVersion) throws IOException, InterruptedException {
                 // FIXME using similar stuff as in the maven plugin could be better 
                 // olamy : but will add a dependency on maven in core -> so not so good 
    -            String mavenVersion = launcher.getChannel().call(new MasterToSlaveCallable<String,IOException>() {
    -                    private static final long serialVersionUID = -4143159957567745621L;
    -
    -                    public String call() throws IOException {
    -                        File[] jars = new File(getHomeDir(),"lib").listFiles();
    -                        if(jars!=null) { // be defensive
    -                            for (File jar : jars) {
    -                                if (jar.getName().startsWith("maven-")) {
    -                                    JarFile jf = null;
    -                                    try {
    -                                        jf = new JarFile(jar);
    -                                        Manifest manifest = jf.getManifest();
    -                                        String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
    -                                        if(version != null) return version;
    -                                    } finally {
    -                                        if(jf != null) jf.close();
    -                                    }
    -                                }
    -                            }
    -                        }
    -                        return "";
    -                    }
    -                });
    +            String mavenVersion = launcher.getChannel().call(new GetMavenVersion());
     
                 if (!mavenVersion.equals("")) {
                     if (mavenReqVersion == MAVEN_20) {
    @@ -594,6 +572,33 @@ public class Maven extends Builder {
                 return false;
                 
             }
    +        private class GetMavenVersion extends MasterToSlaveCallable<String, IOException> {
    +            private static final long serialVersionUID = -4143159957567745621L;
    +            @Override
    +            public String call() throws IOException {
    +                File[] jars = new File(getHomeDir(), "lib").listFiles();
    +                if (jars != null) { // be defensive
    +                    for (File jar : jars) {
    +                        if (jar.getName().startsWith("maven-")) {
    +                            JarFile jf = null;
    +                            try {
    +                                jf = new JarFile(jar);
    +                                Manifest manifest = jf.getManifest();
    +                                String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
    +                                if (version != null) {
    +                                    return version;
    +                                }
    +                            } finally {
    +                                if (jf != null) {
    +                                    jf.close();
    +                                }
    +                            }
    +                        }
    +                    }
    +                }
    +                return "";
    +            }
    +        }
             
             /**
              * Is this Maven 2.1.x or 2.2.x - but not Maven 3.x?
    @@ -609,9 +614,11 @@ public class Maven extends Builder {
              * Gets the executable path of this maven on the given target system.
              */
             public String getExecutable(Launcher launcher) throws IOException, InterruptedException {
    -            return launcher.getChannel().call(new MasterToSlaveCallable<String,IOException>() {
    +            return launcher.getChannel().call(new GetExecutable());
    +        }
    +        private class GetExecutable extends MasterToSlaveCallable<String, IOException> {
                     private static final long serialVersionUID = 2373163112639943768L;
    -
    +                @Override
                     public String call() throws IOException {
                         File exe = getExeFile("mvn");
                         if(exe.exists())
    @@ -621,7 +628,6 @@ public class Maven extends Builder {
                             return exe.getPath();
                         return null;
                     }
    -            });
             }
     
             private File getExeFile(String execName) {
    @@ -709,6 +715,27 @@ public class Maven extends Builder {
                     return ((MavenInstallation)obj).mavenHome;
                 }
             }
    +
    +        @Override
    +        public boolean equals(final Object o) {
    +            if (this == o) return true;
    +            if (o == null || getClass() != o.getClass()) return false;
    +
    +            final MavenInstallation that = (MavenInstallation) o;
    +
    +            if (getHome() != null ? !getHome().equals(that.getHome()) : that.getHome() != null) return false;
    +            if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) return false;
    +            return true;
    +        }
    +
    +        @Override
    +        public int hashCode() {
    +            int result = getHome() != null ? getHome().hashCode() : 0;
    +            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
    +            //result = 31 * result + (getProperties() != null ? getProperties().hashCode() : 0);
    +            return result;
    +        }
    +
         }
     
         /**
    @@ -752,7 +779,7 @@ public class Maven extends Builder {
              * If the Maven installation can not be uniquely determined,
              * it's often better to return just one of them, rather than returning
              * null, since this method is currently ultimately only used to
    -         * decide where to parse <tt>conf/settings.xml</tt> from.
    +         * decide where to parse {@code conf/settings.xml} from.
              */
             MavenInstallation inferMavenInstallation();
         }
    diff --git a/core/src/main/java/hudson/tasks/Shell.java b/core/src/main/java/hudson/tasks/Shell.java
    index 4360f4ce94c9bf74e8c83ecd164765a4c23d8148..3455076d5bfcc9550dd910fdc8e4ba9962113f33 100644
    --- a/core/src/main/java/hudson/tasks/Shell.java
    +++ b/core/src/main/java/hudson/tasks/Shell.java
    @@ -27,6 +27,7 @@ import hudson.FilePath;
     import hudson.Util;
     import hudson.Extension;
     import hudson.model.AbstractProject;
    +import hudson.model.PersistentDescriptor;
     import hudson.remoting.VirtualChannel;
     import hudson.util.FormValidation;
     import java.io.IOException;
    @@ -131,16 +132,12 @@ public class Shell extends CommandInterpreter {
         }
     
         @Extension @Symbol("shell")
    -    public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
    +    public static class DescriptorImpl extends BuildStepDescriptor<Builder> implements PersistentDescriptor {
             /**
              * Shell executable, or null to default.
              */
             private String shell;
     
    -        public DescriptorImpl() {
    -            load();
    -        }
    -
             public boolean isApplicable(Class<? extends AbstractProject> jobType) {
                 return true;
             }
    diff --git a/core/src/main/java/hudson/tasks/package.html b/core/src/main/java/hudson/tasks/package.html
    index 9a85792fab9ecd3e347cf3c850ac37bb8e42ccc2..3ecc377e257b81333f1027a871b92d26d27adca9 100644
    --- a/core/src/main/java/hudson/tasks/package.html
    +++ b/core/src/main/java/hudson/tasks/package.html
    @@ -23,6 +23,6 @@ THE SOFTWARE.
     -->
     
     <html><head/><body>
    -Built-in <a href="Builder.html"><tt>Builder</tt></a>s and <a href="Publisher.html"><tt>Publisher</tt></a>s
    +Built-in <a href="Builder.html"><code>Builder</code></a>s and <a href="Publisher.html"><code>Publisher</code></a>s
     that perform the actual heavy-lifting of a build. 
    -</body></html>
    \ No newline at end of file
    +</body></html>
    diff --git a/core/src/main/java/hudson/tools/BatchCommandInstaller.java b/core/src/main/java/hudson/tools/BatchCommandInstaller.java
    index 48b6a8d24bfc08343820ac8e1bd429d4db6acdfe..f163b74a3a0340b28b9f8e6c6b8a20ea781182ee 100644
    --- a/core/src/main/java/hudson/tools/BatchCommandInstaller.java
    +++ b/core/src/main/java/hudson/tools/BatchCommandInstaller.java
    @@ -33,8 +33,8 @@ import java.io.ObjectStreamException;
     
     /**
      * Installs tool via script execution of Batch script.
    - * Inspired by "Command installer" from the Jenkins core.
    - * @since 0.1
    + * 
    + * @since 1.549
      */
     public class BatchCommandInstaller extends AbstractCommandInstaller {
     
    diff --git a/core/src/main/java/hudson/tools/DownloadFromUrlInstaller.java b/core/src/main/java/hudson/tools/DownloadFromUrlInstaller.java
    index e60facbf6851c3365b20be2dcb963615cd35bbcb..c421b041a9f5288657ffd28a04158706ed4a5652 100644
    --- a/core/src/main/java/hudson/tools/DownloadFromUrlInstaller.java
    +++ b/core/src/main/java/hudson/tools/DownloadFromUrlInstaller.java
    @@ -108,7 +108,7 @@ public abstract class DownloadFromUrlInstaller extends ToolInstaller {
          *
          * @return
          *      Return the real top directory inside {@code root} that contains the meat. In the above example,
    -     *      <tt>root.child("jakarta-ant")</tt> should be returned. If there's no directory to pull up,
    +     *      {@code root.child("jakarta-ant")} should be returned. If there's no directory to pull up,
          *      return null. 
          */
         protected FilePath findPullUpDirectory(FilePath root) throws IOException, InterruptedException {
    diff --git a/core/src/main/java/hudson/tools/JDKInstaller.java b/core/src/main/java/hudson/tools/JDKInstaller.java
    deleted file mode 100644
    index bb316fc1045ff27f46884d3b8deab5c6d61cf253..0000000000000000000000000000000000000000
    --- a/core/src/main/java/hudson/tools/JDKInstaller.java
    +++ /dev/null
    @@ -1,937 +0,0 @@
    -/*
    - * The MIT License
    - *
    - * Copyright (c) 2009-2010, Sun Microsystems, Inc., CloudBees, Inc.
    - *
    - * Permission is hereby granted, free of charge, to any person obtaining a copy
    - * of this software and associated documentation files (the "Software"), to deal
    - * in the Software without restriction, including without limitation the rights
    - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    - * copies of the Software, and to permit persons to whom the Software is
    - * furnished to do so, subject to the following conditions:
    - *
    - * The above copyright notice and this permission notice shall be included in
    - * all copies or substantial portions of the Software.
    - *
    - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    - * THE SOFTWARE.
    - */
    -package hudson.tools;
    -
    -import hudson.AbortException;
    -import hudson.Extension;
    -import hudson.FilePath;
    -import hudson.Launcher;
    -import hudson.Launcher.ProcStarter;
    -import hudson.ProxyConfiguration;
    -import hudson.Util;
    -import hudson.model.DownloadService.Downloadable;
    -import hudson.model.JDK;
    -import hudson.model.Node;
    -import hudson.model.TaskListener;
    -import hudson.util.ArgumentListBuilder;
    -import hudson.util.FormValidation;
    -import hudson.util.HttpResponses;
    -import hudson.util.Secret;
    -import java.io.OutputStream;
    -import java.nio.file.Files;
    -import java.nio.file.InvalidPathException;
    -import jenkins.model.Jenkins;
    -import jenkins.security.MasterToSlaveCallable;
    -import net.sf.json.JSONObject;
    -import net.sf.json.JsonConfig;
    -import org.apache.commons.httpclient.Cookie;
    -import org.apache.commons.httpclient.HttpClient;
    -import org.apache.commons.httpclient.HttpMethodBase;
    -import org.apache.commons.httpclient.UsernamePasswordCredentials;
    -import org.apache.commons.httpclient.auth.AuthScope;
    -import org.apache.commons.httpclient.methods.GetMethod;
    -import org.apache.commons.httpclient.methods.PostMethod;
    -import org.apache.commons.httpclient.protocol.Protocol;
    -import org.apache.commons.io.IOUtils;
    -import org.jenkinsci.Symbol;
    -import org.kohsuke.stapler.DataBoundConstructor;
    -import org.kohsuke.stapler.HttpResponse;
    -import org.kohsuke.stapler.QueryParameter;
    -import org.kohsuke.stapler.Stapler;
    -
    -import javax.servlet.ServletException;
    -import java.io.ByteArrayInputStream;
    -import java.io.DataInputStream;
    -import java.io.File;
    -import java.io.IOException;
    -import java.io.InputStream;
    -import java.io.InputStreamReader;
    -import java.io.OutputStreamWriter;
    -import java.io.PrintStream;
    -import java.net.URL;
    -import java.util.ArrayList;
    -import java.util.Arrays;
    -import java.util.Iterator;
    -import java.util.LinkedList;
    -import java.util.List;
    -import java.util.Locale;
    -import java.util.logging.Logger;
    -import java.util.regex.Matcher;
    -import java.util.regex.Pattern;
    -
    -import static hudson.tools.JDKInstaller.Preference.*;
    -import org.kohsuke.stapler.interceptor.RequirePOST;
    -
    -/**
    - * Install JDKs from java.sun.com.
    - *
    - * @author Kohsuke Kawaguchi
    - * @since 1.305
    - */
    -public class JDKInstaller extends ToolInstaller {
    -
    -    static {
    -        // this socket factory will not attempt to bind to the client interface
    -        Protocol.registerProtocol("http", new Protocol("http", new hudson.util.NoClientBindProtocolSocketFactory(), 80));
    -        Protocol.registerProtocol("https", new Protocol("https", new hudson.util.NoClientBindSSLProtocolSocketFactory(), 443));
    -    }
    -
    -    /**
    -     * The release ID that Sun assigns to each JDK, such as "jdk-6u13-oth-JPR@CDS-CDS_Developer"
    -     *
    -     * <p>
    -     * This ID can be seen in the "ProductRef" query parameter of the download page, like
    -     * https://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD/ViewProductDetail-Start?ProductRef=jdk-6u13-oth-JPR@CDS-CDS_Developer
    -     */
    -    public final String id;
    -
    -    /**
    -     * We require that the user accepts the license by clicking a checkbox, to make up for the part
    -     * that we auto-accept cds.sun.com license click through.
    -     */
    -    public final boolean acceptLicense;
    -
    -    @DataBoundConstructor
    -    public JDKInstaller(String id, boolean acceptLicense) {
    -        super(null);
    -        this.id = id;
    -        this.acceptLicense = acceptLicense;
    -    }
    -
    -    public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException {
    -        FilePath expectedLocation = preferredLocation(tool, node);
    -        PrintStream out = log.getLogger();
    -        try {
    -            if(!acceptLicense) {
    -                out.println(Messages.JDKInstaller_UnableToInstallUntilLicenseAccepted());
    -                return expectedLocation;
    -            }
    -            // already installed?
    -            FilePath marker = expectedLocation.child(".installedByHudson");
    -            if (marker.exists() && marker.readToString().equals(id)) {
    -                return expectedLocation;
    -            }
    -            expectedLocation.deleteRecursive();
    -            expectedLocation.mkdirs();
    -
    -            Platform p = Platform.of(node);
    -            URL url = locate(log, p, CPU.of(node));
    -
    -//            out.println("Downloading "+url);
    -            FilePath file = expectedLocation.child(p.bundleFileName);
    -            file.copyFrom(url);
    -
    -            // JDK6u13 on Windows doesn't like path representation like "/tmp/foo", so make it a strict platform native format by doing 'absolutize'
    -            install(node.createLauncher(log), p, new FilePathFileSystem(node), log, expectedLocation.absolutize().getRemote(), file.getRemote());
    -
    -            // successfully installed
    -            file.delete();
    -            marker.write(id, null);
    -
    -        } catch (DetectionFailedException e) {
    -            out.println("JDK installation skipped: "+e.getMessage());
    -        }
    -
    -        return expectedLocation;
    -    }
    -
    -    /**
    -     * Performs the JDK installation to a system, provided that the bundle was already downloaded.
    -     *
    -     * @param launcher
    -     *      Used to launch processes on the system.
    -     * @param p
    -     *      Platform of the system. This determines how the bundle is installed.
    -     * @param fs
    -     *      Abstraction of the file system manipulation on this system.
    -     * @param log
    -     *      Where the output from the installation will be written.
    -     * @param expectedLocation
    -     *      Path to install JDK to. Must be absolute and in the native file system notation.
    -     * @param jdkBundle
    -     *      Path to the installed JDK bundle. (The bundle to download can be determined by {@link #locate(TaskListener, Platform, CPU)} call.)
    -     */
    -    public void install(Launcher launcher, Platform p, FileSystem fs, TaskListener log, String expectedLocation, String jdkBundle) throws IOException, InterruptedException {
    -        PrintStream out = log.getLogger();
    -
    -        out.println("Installing "+ jdkBundle);
    -        FilePath parent = new FilePath(launcher.getChannel(), expectedLocation).getParent();
    -        switch (p) {
    -        case LINUX:
    -        case SOLARIS:
    -            // JDK on Unix up to 6 was distributed as shell script installer, but in JDK7 it switched to a plain tgz.
    -            // so check if the file is gzipped, and if so, treat it accordingly
    -            byte[] header = new byte[2];
    -            {
    -                try (InputStream is = fs.read(jdkBundle);
    -                     DataInputStream in = new DataInputStream(is)) {
    -                    in.readFully(header);
    -                }
    -            }
    -
    -            ProcStarter starter;
    -            if (header[0]==0x1F && header[1]==(byte)0x8B) {// gzip
    -                starter = launcher.launch().cmds("tar", "xvzf", jdkBundle);
    -            } else {
    -                fs.chmod(jdkBundle,0755);
    -                starter = launcher.launch().cmds(jdkBundle, "-noregister");
    -            }
    -
    -            int exit = starter
    -                    .stdin(new ByteArrayInputStream("yes".getBytes())).stdout(out)
    -                    .pwd(new FilePath(launcher.getChannel(), expectedLocation)).join();
    -            if (exit != 0)
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -
    -            // JDK creates its own sub-directory, so pull them up
    -            List<String> paths = fs.listSubDirectories(expectedLocation);
    -            for (Iterator<String> itr = paths.iterator(); itr.hasNext();) {
    -                String s =  itr.next();
    -                if (!s.matches("j(2s)?dk.*"))
    -                    itr.remove();
    -            }
    -            if(paths.size()!=1)
    -                throw new AbortException("Failed to find the extracted JDKs: "+paths);
    -
    -            // remove the intermediate directory
    -            fs.pullUp(expectedLocation+'/'+paths.get(0),expectedLocation);
    -            break;
    -        case WINDOWS:
    -            /*
    -                Windows silent installation is full of bad know-how.
    -
    -                On Windows, command line argument to a process at the OS level is a single string,
    -                not a string array like POSIX. When we pass arguments as string array, JRE eventually
    -                turn it into a single string with adding quotes to "the right place". Unfortunately,
    -                with the strange argument layout of InstallShield (like /v/qn" INSTALLDIR=foobar"),
    -                it appears that the escaping done by JRE gets in the way, and prevents the installation.
    -                Presumably because of this, my attempt to use /q/vn" INSTALLDIR=foo" didn't work with JDK5.
    -
    -                I tried to locate exactly how InstallShield parses the arguments (and why it uses
    -                awkward option like /qn, but couldn't find any. Instead, experiments revealed that
    -                "/q/vn ARG ARG ARG" works just as well. This is presumably due to the Visual C++ runtime library
    -                (which does single string -> string array conversion to invoke the main method in most Win32 process),
    -                and this consistently worked on JDK5 and JDK4.
    -
    -                Some of the official documentations are available at
    -                - http://java.sun.com/j2se/1.5.0/sdksilent.html
    -                - http://java.sun.com/j2se/1.4.2/docs/guide/plugin/developer_guide/silent.html
    -             */
    -
    -            expectedLocation = expectedLocation.trim();
    -            if (expectedLocation.endsWith("\\")) {
    -                // Prevent a trailing slash from escaping quotes
    -                expectedLocation = expectedLocation.substring(0, expectedLocation.length() - 1);
    -            }
    -            String logFile = parent.createTempFile("install", "log").getRemote();
    -
    -
    -            ArgumentListBuilder args = new ArgumentListBuilder();
    -            assert (new File(expectedLocation).exists()) : expectedLocation
    -                    + " must exist, otherwise /L will cause the installer to fail with error 1622";
    -            if (isJava15() || isJava14()) {
    -                // Installer uses InstallShield.
    -                args.add("CMD.EXE", "/C");
    -
    -                // see http://docs.oracle.com/javase/1.5.0/docs/guide/deployment/deployment-guide/silent.html
    -                // CMD.EXE /C must be followed by a single parameter (do not split it!)
    -                args.add(jdkBundle + " /s /v\"/qn REBOOT=ReallySuppress INSTALLDIR=\\\""
    -                        + expectedLocation + "\\\" /L \\\"" + logFile + "\\\"\"");
    -            } else {
    -                // Installed uses Windows Installer (MSI)
    -                args.add(jdkBundle, "/s");
    -
    -                // Create a private JRE by omitting "PublicjreFeature"
    -                // @see http://docs.oracle.com/javase/7/docs/webnotes/install/windows/jdk-installation-windows.html#jdk-silent-installation
    -                args.add("ADDLOCAL=\"ToolsFeature\"",
    -                        "REBOOT=ReallySuppress", "INSTALLDIR=" + expectedLocation,
    -                        "/L",  logFile);
    -            }
    -            int r = launcher.launch().cmds(args).stdout(out)
    -                    .pwd(new FilePath(launcher.getChannel(), expectedLocation)).join();
    -            if (r != 0) {
    -                out.println(Messages.JDKInstaller_FailedToInstallJDK(r));
    -                // log file is in UTF-16
    -                try (InputStreamReader in = new InputStreamReader(fs.read(logFile), "UTF-16")) {
    -                    IOUtils.copy(in, new OutputStreamWriter(out));
    -                }
    -                throw new AbortException();
    -            }
    -
    -            fs.delete(logFile);
    -
    -            break;
    -
    -        case OSX:
    -            // Mount the DMG distribution bundle
    -            FilePath dmg = parent.createTempDir("jdk", "dmg");
    -            exit = launcher.launch()
    -                    .cmds("hdiutil", "attach", "-puppetstrings", "-mountpoint", dmg.getRemote(), jdkBundle)
    -                    .stdout(log)
    -                    .join();
    -            if (exit != 0)
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -
    -            // expand the installation PKG
    -            FilePath[] list = dmg.list("*.pkg");
    -            if (list.length != 1) {
    -                log.getLogger().println("JDK dmg bundle does not contain expected pkg installer");
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -            }
    -            String installer = list[0].getRemote();
    -
    -            FilePath pkg = parent.createTempDir("jdk", "pkg");
    -            pkg.deleteRecursive(); // pkgutil fails if target directory exists
    -            exit = launcher.launch()
    -                    .cmds("pkgutil", "--expand", installer, pkg.getRemote())
    -                    .stdout(log)
    -                    .join();
    -            if (exit != 0)
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -
    -            exit = launcher.launch()
    -                    .cmds("umount", dmg.getRemote())
    -                    .stdout(log)
    -                    .join();
    -            if (exit != 0)
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -
    -            // We only want the actual JDK sub-package, which "Payload" is actually a tar.gz archive
    -            list = pkg.list("jdk*.pkg/Payload");
    -            if (list.length != 1) {
    -                log.getLogger().println("JDK pkg installer does not contain expected JDK Payload archive");
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -            }
    -            String payload = list[0].getRemote();
    -            exit = launcher.launch()
    -                    .pwd(parent).cmds("tar", "xzf", payload)
    -                    .stdout(log)
    -                    .join();
    -            if (exit != 0)
    -                throw new AbortException(Messages.JDKInstaller_FailedToInstallJDK(exit));
    -
    -            parent.child("Contents/Home").moveAllChildrenTo(new FilePath(launcher.getChannel(), expectedLocation));
    -            parent.child("Contents").deleteRecursive();
    -
    -            pkg.deleteRecursive();
    -            dmg.deleteRecursive();
    -            break;
    -        }
    -    }
    -
    -    private boolean isJava15() {
    -        return id.contains("-1.5");
    -    }
    -
    -    private boolean isJava14() {
    -        return id.contains("-1.4");
    -    }
    -
    -    /**
    -     * Abstraction of the file system to perform JDK installation.
    -     * Consider {@link JDKInstaller.FilePathFileSystem} as the canonical documentation of the contract.
    -     */
    -    public interface FileSystem {
    -        void delete(String file) throws IOException, InterruptedException;
    -        void chmod(String file,int mode) throws IOException, InterruptedException;
    -        InputStream read(String file) throws IOException, InterruptedException;
    -        /**
    -         * List sub-directories of the given directory and just return the file name portion.
    -         */
    -        List<String> listSubDirectories(String dir) throws IOException, InterruptedException;
    -        void pullUp(String from, String to) throws IOException, InterruptedException;
    -    }
    -
    -    /*package*/ static final class FilePathFileSystem implements FileSystem {
    -        private final Node node;
    -
    -        FilePathFileSystem(Node node) {
    -            this.node = node;
    -        }
    -
    -        public void delete(String file) throws IOException, InterruptedException {
    -            $(file).delete();
    -        }
    -
    -        public void chmod(String file, int mode) throws IOException, InterruptedException {
    -            $(file).chmod(mode);
    -        }
    -
    -        public InputStream read(String file) throws IOException, InterruptedException {
    -            return $(file).read();
    -        }
    -
    -        public List<String> listSubDirectories(String dir) throws IOException, InterruptedException {
    -            List<String> r = new ArrayList<String>();
    -            for( FilePath f : $(dir).listDirectories())
    -                r.add(f.getName());
    -            return r;
    -        }
    -
    -        public void pullUp(String from, String to) throws IOException, InterruptedException {
    -            $(from).moveAllChildrenTo($(to));
    -        }
    -
    -        private FilePath $(String file) {
    -            return node.createPath(file);
    -        }
    -    }
    -
    -    /**
    -     * This is where we locally cache this JDK.
    -     */
    -    private File getLocalCacheFile(Platform platform, CPU cpu) {
    -        return new File(Jenkins.getInstance().getRootDir(),"cache/jdks/"+platform+"/"+cpu+"/"+id);
    -    }
    -
    -    /**
    -     * Performs a license click through and obtains the one-time URL for downloading bits.
    -     */
    -    public URL locate(TaskListener log, Platform platform, CPU cpu) throws IOException {
    -        File cache = getLocalCacheFile(platform, cpu);
    -        if (cache.exists() && cache.length()>1*1024*1024) return cache.toURL(); // if the file is too small, don't trust it. In the past, the download site served error message in 200 status code
    -
    -        log.getLogger().println("Installing JDK "+id);
    -        JDKFamilyList families = JDKList.all().get(JDKList.class).toList();
    -        if (families.isEmpty())
    -            throw new IOException("JDK data is empty.");
    -
    -        JDKRelease release = families.getRelease(id);
    -        if (release==null)
    -            throw new IOException("Unable to find JDK with ID="+id);
    -
    -        JDKFile primary=null,secondary=null;
    -        for (JDKFile f : release.files) {
    -            String vcap = f.name.toUpperCase(Locale.ENGLISH);
    -
    -            // JDK files have either 'windows', 'linux', or 'solaris' in its name, so that allows us to throw
    -            // away unapplicable stuff right away
    -            if(!platform.is(vcap))
    -                continue;
    -
    -            switch (cpu.accept(vcap)) {
    -            case PRIMARY:   primary = f;break;
    -            case SECONDARY: secondary=f;break;
    -            case UNACCEPTABLE:  break;
    -            }
    -        }
    -
    -        if(primary==null)   primary=secondary;
    -        if(primary==null)
    -            throw new AbortException("Couldn't find the right download for "+platform+" and "+ cpu +" combination");
    -        LOGGER.fine("Platform choice:"+primary);
    -
    -        log.getLogger().println("Downloading JDK from "+primary.filepath);
    -
    -        HttpClient hc = new HttpClient();
    -        hc.getParams().setParameter("http.useragent","Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)");
    -        ProxyConfiguration jpc = Jenkins.getInstance().proxy;
    -        if(jpc != null) {
    -            hc.getHostConfiguration().setProxy(jpc.name, jpc.port);
    -            if(jpc.getUserName() != null)
    -                hc.getState().setProxyCredentials(AuthScope.ANY,new UsernamePasswordCredentials(jpc.getUserName(),jpc.getPassword()));
    -        }
    -
    -        int authCount=0, totalPageCount=0;  // counters for avoiding infinite loop
    -
    -        HttpMethodBase m = new GetMethod(primary.filepath);
    -        hc.getState().addCookie(new Cookie(".oracle.com","gpw_e24",".", "/", -1, false));
    -        hc.getState().addCookie(new Cookie(".oracle.com","oraclelicense","accept-securebackup-cookie", "/", -1, false));
    -        try {
    -            while (true) {
    -                if (totalPageCount++>16) // looping too much
    -                    throw new IOException("Unable to find the login form");
    -
    -                LOGGER.fine("Requesting " + m.getURI());
    -                int r = hc.executeMethod(m);
    -                if (r/100==3) {
    -                    // redirect?
    -                    String loc = m.getResponseHeader("Location").getValue();
    -                    m.releaseConnection();
    -                    m = new GetMethod(loc);
    -                    continue;
    -                }
    -                if (r!=200)
    -                    throw new IOException("Failed to request " + m.getURI() +" exit code="+r);
    -
    -                if (m.getURI().getHost().equals("login.oracle.com")) {
    -                    LOGGER.fine("Appears to be a login page");
    -                    String resp = IOUtils.toString(m.getResponseBodyAsStream(), m.getResponseCharSet());
    -                    m.releaseConnection();
    -                    Matcher pm = Pattern.compile("<form .*?action=\"([^\"]*)\" .*?</form>", Pattern.DOTALL).matcher(resp);
    -                    if (!pm.find())
    -                        throw new IllegalStateException("Unable to find a form in the response:\n"+resp);
    -
    -                    String form = pm.group();
    -                    PostMethod post = new PostMethod(
    -                            new URL(new URL(m.getURI().getURI()),pm.group(1)).toExternalForm());
    -
    -                    String u = getDescriptor().getUsername();
    -                    Secret p = getDescriptor().getPassword();
    -                    if (u==null || p==null) {
    -                        log.hyperlink(getCredentialPageUrl(),"Oracle now requires Oracle account to download previous versions of JDK. Please specify your Oracle account username/password.\n");
    -                        throw new AbortException("Unable to install JDK unless a valid Oracle account username/password is provided in the system configuration.");
    -                    }
    -
    -                    for (String fragment : form.split("<input")) {
    -                        String n = extractAttribute(fragment,"name");
    -                        String v = extractAttribute(fragment,"value");
    -                        if (n==null || v==null)     continue;
    -                        if (n.equals("ssousername"))
    -                            v = u;
    -                        if (n.equals("password")) {
    -                            v = p.getPlainText();
    -                            if (authCount++ > 3) {
    -                                log.hyperlink(getCredentialPageUrl(),"Your Oracle account doesn't appear valid. Please specify a valid username/password\n");
    -                                throw new AbortException("Unable to install JDK unless a valid username/password is provided.");
    -                            }
    -                        }
    -                        post.addParameter(n, v);
    -                    }
    -
    -                    m = post;
    -                } else {
    -                    log.getLogger().println("Downloading " + m.getResponseContentLength() + " bytes");
    -
    -                    // download to a temporary file and rename it in to handle concurrency and failure correctly,
    -                    File tmp = new File(cache.getPath()+".tmp");
    -                    try {
    -                        tmp.getParentFile().mkdirs();
    -                        try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
    -                            IOUtils.copy(m.getResponseBodyAsStream(), out);
    -                        } catch (InvalidPathException e) {
    -                            throw new IOException(e);
    -                        }
    -
    -                        tmp.renameTo(cache);
    -                        return cache.toURL();
    -                    } finally {
    -                        tmp.delete();
    -                    }
    -                }
    -            }
    -        } finally {
    -            m.releaseConnection();
    -        }
    -    }
    -
    -    private static String extractAttribute(String s, String name) {
    -        String h = name + "=\"";
    -        int si = s.indexOf(h);
    -        if (si<0)   return null;
    -        int ei = s.indexOf('\"',si+h.length());
    -        return s.substring(si+h.length(),ei);
    -    }
    -
    -    private String getCredentialPageUrl() {
    -        return "/"+getDescriptor().getDescriptorUrl()+"/enterCredential";
    -    }
    -
    -    public enum Preference {
    -        PRIMARY, SECONDARY, UNACCEPTABLE
    -    }
    -
    -    /**
    -     * Supported platform.
    -     */
    -    public enum Platform {
    -        LINUX("jdk.sh"), SOLARIS("jdk.sh"), WINDOWS("jdk.exe"), OSX("jdk.dmg");
    -
    -        /**
    -         * Choose the file name suitable for the downloaded JDK bundle.
    -         */
    -        public final String bundleFileName;
    -
    -        Platform(String bundleFileName) {
    -            this.bundleFileName = bundleFileName;
    -        }
    -
    -        public boolean is(String line) {
    -            return line.contains(name());
    -        }
    -
    -        /**
    -         * Determines the platform of the given node.
    -         */
    -        public static Platform of(Node n) throws IOException,InterruptedException,DetectionFailedException {
    -            return n.getChannel().call(new GetCurrentPlatform());
    -        }
    -
    -        public static Platform current() throws DetectionFailedException {
    -            String arch = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
    -            if(arch.contains("linux"))  return LINUX;
    -            if(arch.contains("windows"))   return WINDOWS;
    -            if(arch.contains("sun") || arch.contains("solaris"))    return SOLARIS;
    -            if(arch.contains("mac")) return OSX;
    -            throw new DetectionFailedException("Unknown CPU name: "+arch);
    -        }
    -
    -        static class GetCurrentPlatform extends MasterToSlaveCallable<Platform,DetectionFailedException> {
    -            private static final long serialVersionUID = 1L;
    -            public Platform call() throws DetectionFailedException {
    -                return current();
    -            }
    -        }
    -
    -    }
    -
    -    /**
    -     * CPU type.
    -     */
    -    public enum CPU {
    -        i386, amd64, Sparc, Itanium;
    -
    -        /**
    -         * In JDK5u3, I see platform like "Linux AMD64", while JDK6u3 refers to "Linux x64", so
    -         * just use "64" for locating bits.
    -         */
    -        public Preference accept(String line) {
    -            switch (this) {
    -            // these two guys are totally incompatible with everything else, so no fallback
    -            case Sparc:     return must(line.contains("SPARC"));
    -            case Itanium:   return must(line.contains("IA64"));
    -
    -            // 64bit Solaris, Linux, and Windows can all run 32bit executable, so fall back to 32bit if 64bit bundle is not found
    -            case amd64:
    -                if(line.contains("SPARC") || line.contains("IA64"))  return UNACCEPTABLE;
    -                if(line.contains("64"))     return PRIMARY;
    -                return SECONDARY;
    -            case i386:
    -                if(line.contains("64") || line.contains("SPARC") || line.contains("IA64"))     return UNACCEPTABLE;
    -                return PRIMARY;
    -            }
    -            return UNACCEPTABLE;
    -        }
    -
    -        private static Preference must(boolean b) {
    -             return b ? PRIMARY : UNACCEPTABLE;
    -        }
    -
    -        /**
    -         * Determines the CPU of the given node.
    -         */
    -        public static CPU of(Node n) throws IOException,InterruptedException, DetectionFailedException {
    -            return n.getChannel().call(new GetCurrentCPU());
    -        }
    -
    -        /**
    -         * Determines the CPU of the current JVM.
    -         *
    -         * http://lopica.sourceforge.net/os.html was useful in writing this code.
    -         */
    -        public static CPU current() throws DetectionFailedException {
    -            String arch = System.getProperty("os.arch").toLowerCase(Locale.ENGLISH);
    -            if(arch.contains("sparc"))  return Sparc;
    -            if(arch.contains("ia64"))   return Itanium;
    -            if(arch.contains("amd64") || arch.contains("86_64"))    return amd64;
    -            if(arch.contains("86"))    return i386;
    -            throw new DetectionFailedException("Unknown CPU architecture: "+arch);
    -        }
    -
    -        static class GetCurrentCPU extends MasterToSlaveCallable<CPU,DetectionFailedException> {
    -            private static final long serialVersionUID = 1L;
    -            public CPU call() throws DetectionFailedException {
    -                return current();
    -            }
    -        }
    -
    -    }
    -
    -    /**
    -     * Indicates the failure to detect the OS or CPU.
    -     */
    -    private static final class DetectionFailedException extends Exception {
    -        private DetectionFailedException(String message) {
    -            super(message);
    -        }
    -    }
    -
    -    public static final class JDKFamilyList {
    -        public JDKFamily[] data = new JDKFamily[0];
    -        public int version;
    -
    -        public boolean isEmpty() {
    -            for (JDKFamily f : data) {
    -                if (f.releases.length>0)
    -                    return false;
    -            }
    -            return true;
    -        }
    -
    -        public JDKRelease getRelease(String productCode) {
    -            for (JDKFamily f : data) {
    -                for (JDKRelease r : f.releases) {
    -                    if (r.matchesId(productCode))
    -                        return r;
    -                }
    -            }
    -            return null;
    -        }
    -    }
    -
    -    public static final class JDKFamily {
    -        public String name;
    -        public JDKRelease[] releases;
    -    }
    -
    -    public static final class JDKRelease {
    -        /**
    -         * the list of {@link JDKFile}s
    -         */
    -        public JDKFile[] files;
    -        /**
    -         * the license path
    -         */
    -        public String licpath;
    -        /**
    -         * the license title
    -         */
    -        public String lictitle;
    -        /**
    -         * This maps to the former product code, like "jdk-6u13-oth-JPR"
    -         */
    -        public String name;
    -        /**
    -         * This is human readable.
    -         */
    -        public String title;
    -
    -        /**
    -         * We used to use IDs like "jdk-6u13-oth-JPR@CDS-CDS_Developer", but Oracle switched to just "jdk-6u13-oth-JPR".
    -         * This method matches if the specified string matches the name, and it accepts both the old and the new format.
    -         */
    -        public boolean matchesId(String rhs) {
    -            return rhs!=null && (rhs.equals(name) || rhs.startsWith(name+"@"));
    -        }
    -    }
    -
    -    public static final class JDKFile {
    -        public String filepath;
    -        public String name;
    -        public String title;
    -    }
    -
    -    @Override
    -    public DescriptorImpl getDescriptor() {
    -        return (DescriptorImpl)super.getDescriptor();
    -    }
    -
    -    @Extension @Symbol("jdkInstaller")
    -    public static final class DescriptorImpl extends ToolInstallerDescriptor<JDKInstaller> {
    -        private String username;
    -        private Secret password;
    -
    -        public DescriptorImpl() {
    -            load();
    -        }
    -
    -        public String getDisplayName() {
    -            return Messages.JDKInstaller_DescriptorImpl_displayName();
    -        }
    -
    -        @Override
    -        public boolean isApplicable(Class<? extends ToolInstallation> toolType) {
    -            return toolType==JDK.class;
    -        }
    -
    -        public String getUsername() {
    -            return username;
    -        }
    -
    -        public Secret getPassword() {
    -            return password;
    -        }
    -
    -        public FormValidation doCheckId(@QueryParameter String value) {
    -            if (Util.fixEmpty(value) == null)
    -                return FormValidation.error(Messages.JDKInstaller_DescriptorImpl_doCheckId()); // improve message
    -            return FormValidation.ok();
    -        }
    -
    -        /**
    -         * List of installable JDKs.
    -         * @return never null.
    -         */
    -        public List<JDKFamily> getInstallableJDKs() throws IOException {
    -            return Arrays.asList(JDKList.all().get(JDKList.class).toList().data);
    -        }
    -
    -        public FormValidation doCheckAcceptLicense(@QueryParameter boolean value) {
    -            if (username==null || password==null)
    -                return FormValidation.errorWithMarkup(Messages.JDKInstaller_RequireOracleAccount(Stapler.getCurrentRequest().getContextPath()+'/'+getDescriptorUrl()+"/enterCredential"));
    -            if (value) {
    -                return FormValidation.ok();
    -            } else {
    -                return FormValidation.error(Messages.JDKInstaller_DescriptorImpl_doCheckAcceptLicense());
    -            }
    -        }
    -
    -        /**
    -         * Submits the Oracle account username/password.
    -         */
    -        @RequirePOST
    -        public HttpResponse doPostCredential(@QueryParameter String username, @QueryParameter String password) throws IOException, ServletException {
    -            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    -            this.username = username;
    -            this.password = Secret.fromString(password);
    -            save();
    -            return HttpResponses.redirectTo("credentialOK");
    -        }
    -    }
    -
    -    /**
    -     * JDK list.
    -     */
    -    @Extension @Symbol("jdk")
    -    public static final class JDKList extends Downloadable {
    -        public JDKList() {
    -            super(JDKInstaller.class);
    -        }
    -
    -        public JDKFamilyList toList() throws IOException {
    -            JSONObject d = getData();
    -            if(d==null) return new JDKFamilyList();
    -            return (JDKFamilyList)JSONObject.toBean(d,JDKFamilyList.class);
    -        }
    -
    -        /**
    -         * {@inheritDoc}
    -         */
    -        @Override
    -        public JSONObject reduce (List<JSONObject> jsonObjectList) {
    -            List<JDKFamily> reducedFamilies = new LinkedList<>();
    -            int version = 0;
    -            JsonConfig jsonConfig = new JsonConfig();
    -            jsonConfig.registerPropertyExclusion(JDKFamilyList.class, "empty");
    -            jsonConfig.setRootClass(JDKFamilyList.class);
    -            //collect all JDKFamily objects from the multiple json objects
    -            for (JSONObject jsonJdkFamilyList : jsonObjectList) {
    -                JDKFamilyList jdkFamilyList = (JDKFamilyList)JSONObject.toBean(jsonJdkFamilyList, jsonConfig);
    -                if (version == 0) {
    -                    //we set as version the version of the first update center
    -                    version = jdkFamilyList.version;
    -                }
    -                JDKFamily[] jdkFamilies = jdkFamilyList.data;
    -                reducedFamilies.addAll(Arrays.asList(jdkFamilies));
    -            }
    -            //we  iterate on the list and reduce it until there are no more duplicates
    -            //this could be made recursive
    -            while (hasDuplicates(reducedFamilies, "name")) {
    -                //create a temporary list to store the tmp result
    -                List<JDKFamily> tmpReducedFamilies = new LinkedList<>();
    -                //we need to skip the processed families
    -                boolean processed [] = new boolean[reducedFamilies.size()];
    -                for (int i = 0; i < reducedFamilies.size(); i ++ ) {
    -                    if (processed [i] == true) {
    -                        continue;
    -                    }
    -                    JDKFamily data1 = reducedFamilies.get(i);
    -                    boolean hasDuplicate = false;
    -                    for (int j = i + 1; j < reducedFamilies.size(); j ++ ) {
    -                        JDKFamily data2 = reducedFamilies.get(j);
    -                        //if we found a duplicate we need to merge the families
    -                        if (data1.name.equals(data2.name)) {
    -                            hasDuplicate = true;
    -                            processed [j] = true;
    -                            JDKFamily reducedData = reduceData(data1.name, new LinkedList<JDKRelease>(Arrays.asList(data1.releases)), new LinkedList<JDKRelease>(Arrays.asList(data2.releases)));
    -                            tmpReducedFamilies.add(reducedData);
    -                            //after the first duplicate has been found we break the loop since the duplicates are
    -                            //processed two by two
    -                            break;
    -                        }
    -                    }
    -                    //if no duplicate has been found we just insert the whole family in the tmp list
    -                    if (!hasDuplicate) {
    -                        tmpReducedFamilies.add(data1);
    -                    }
    -                }
    -                reducedFamilies = tmpReducedFamilies;
    -            }
    -            JDKFamilyList jdkFamilyList = new JDKFamilyList();
    -            jdkFamilyList.version = version;
    -            jdkFamilyList.data = new JDKFamily[reducedFamilies.size()];
    -            reducedFamilies.toArray(jdkFamilyList.data);
    -            JSONObject reducedJdkFamilyList = JSONObject.fromObject(jdkFamilyList, jsonConfig);
    -            //return the list with no duplicates
    -            return reducedJdkFamilyList;
    -        }
    -
    -        private JDKFamily reduceData(String name, List<JDKRelease> releases1, List<JDKRelease> releases2) {
    -            LinkedList<JDKRelease> reducedReleases = new LinkedList<>();
    -            for (Iterator<JDKRelease> iterator = releases1.iterator(); iterator.hasNext(); ) {
    -                JDKRelease release1 = iterator.next();
    -                boolean hasDuplicate = false;
    -                for (Iterator<JDKRelease> iterator2 = releases2.iterator(); iterator2.hasNext(); ) {
    -                    JDKRelease release2 = iterator2.next();
    -                    if (release1.name.equals(release2.name)) {
    -                        hasDuplicate = true;
    -                        JDKRelease reducedRelease = reduceReleases(release1, new LinkedList<JDKFile>(Arrays.asList(release1.files)), new LinkedList<JDKFile>(Arrays.asList(release2.files)));
    -                        iterator2.remove();
    -                        reducedReleases.add(reducedRelease);
    -                        //we assume that in one release list there are no duplicates so we stop at the first one
    -                        break;
    -                    }
    -                }
    -                if (!hasDuplicate) {
    -                    reducedReleases.add(release1);
    -                }
    -            }
    -            reducedReleases.addAll(releases2);
    -            JDKFamily reducedFamily = new JDKFamily();
    -            reducedFamily.name = name;
    -            reducedFamily.releases = new JDKRelease[reducedReleases.size()];
    -            reducedReleases.toArray(reducedFamily.releases);
    -            return reducedFamily;
    -        }
    -
    -        private JDKRelease reduceReleases(JDKRelease release, List<JDKFile> files1, List<JDKFile> files2) {
    -            LinkedList<JDKFile> reducedFiles = new LinkedList<>();
    -            for (Iterator<JDKFile> iterator1 = files1.iterator(); iterator1.hasNext(); ) {
    -                JDKFile file1 = iterator1.next();
    -                for (Iterator<JDKFile> iterator2 = files2.iterator(); iterator2.hasNext(); ) {
    -                    JDKFile file2 = iterator2.next();
    -                    if (file1.name.equals(file2.name)) {
    -                        iterator2.remove();
    -                        //we assume the in one file list there are no duplicates so we break after we find the
    -                        //first match
    -                        break;
    -                    }
    -                }
    -            }
    -            reducedFiles.addAll(files1);
    -            reducedFiles.addAll(files2);
    -
    -            JDKRelease jdkRelease = new JDKRelease();
    -            jdkRelease.files = new JDKFile[reducedFiles.size()];
    -            reducedFiles.toArray(jdkRelease.files);
    -            jdkRelease.name = release.name;
    -            jdkRelease.licpath = release.licpath;
    -            jdkRelease.lictitle = release.lictitle;
    -            jdkRelease.title = release.title;
    -            return jdkRelease;
    -        }
    -    }
    -
    -    private static final Logger LOGGER = Logger.getLogger(JDKInstaller.class.getName());
    -}
    diff --git a/core/src/main/java/hudson/tools/ToolDescriptor.java b/core/src/main/java/hudson/tools/ToolDescriptor.java
    index 3c944d86dc31c9b326268bcf8c361ed25a27e337..c39149ceebe3dd675ea7c9572a1b3106a30ae141 100644
    --- a/core/src/main/java/hudson/tools/ToolDescriptor.java
    +++ b/core/src/main/java/hudson/tools/ToolDescriptor.java
    @@ -44,6 +44,8 @@ import org.jvnet.tiger_types.Types;
     import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerRequest;
     
    +import javax.annotation.Nonnull;
    +
     /**
      * {@link Descriptor} for {@link ToolInstallation}.
      *
    @@ -54,6 +56,15 @@ public abstract class ToolDescriptor<T extends ToolInstallation> extends Descrip
     
         private T[] installations;
     
    +    protected ToolDescriptor() { }
    +
    +    /**
    +     * @since 2.102
    +     */
    +    protected ToolDescriptor(Class<T> clazz) {
    +        super(clazz);
    +    }
    +
         /**
          * Configured instances of {@link ToolInstallation}s.
          *
    @@ -100,12 +111,12 @@ public abstract class ToolDescriptor<T extends ToolInstallation> extends Descrip
          * Lists up {@link ToolPropertyDescriptor}s that are applicable to this {@link ToolInstallation}.
          */
         public List<ToolPropertyDescriptor> getPropertyDescriptors() {
    -        return PropertyDescriptor.<ToolPropertyDescriptor, ToolInstallation>for_(ToolProperty.all(), clazz);
    +        return PropertyDescriptor.for_(ToolProperty.all(), clazz);
         }
     
     
         @Override
    -    public GlobalConfigurationCategory getCategory() {
    +    public @Nonnull GlobalConfigurationCategory getCategory() {
             return GlobalConfigurationCategory.get(ToolConfigurationCategory.class);
         }
     
    diff --git a/core/src/main/java/hudson/tools/ToolInstaller.java b/core/src/main/java/hudson/tools/ToolInstaller.java
    index 2aaec73d1d3787244e1e9b776479a2fdf2634698..0a6ef5d07c74d5e5436fc56652556fdb8c71c66b 100644
    --- a/core/src/main/java/hudson/tools/ToolInstaller.java
    +++ b/core/src/main/java/hudson/tools/ToolInstaller.java
    @@ -42,12 +42,14 @@ import org.kohsuke.stapler.DataBoundConstructor;
     
     /**
      * An object which can ensure that a generic {@link ToolInstallation} in fact exists on a node.
    + * The properties can be added to {@link ToolInstallation} using the {@link InstallSourceProperty}.
      *
      * The subclass should have a {@link ToolInstallerDescriptor}.
      * A {@code config.jelly} should be provided to customize specific fields;
      * {@code <t:label xmlns:t="/hudson/tools"/>} to customize {@code label}.
      * @see <a href="http://wiki.jenkins-ci.org/display/JENKINS/Tool+Auto-Installation">Tool Auto-Installation</a>
      * @since 1.305
    + * @see InstallSourceProperty
      */
     public abstract class ToolInstaller implements Describable<ToolInstaller>, ExtensionPoint {
     
    diff --git a/core/src/main/java/hudson/tools/ZipExtractionInstaller.java b/core/src/main/java/hudson/tools/ZipExtractionInstaller.java
    index ba91cfe45763c984eb0e6de05ee817ed51546031..af9bffda1126090623f361f0919fe74e94769d27 100644
    --- a/core/src/main/java/hudson/tools/ZipExtractionInstaller.java
    +++ b/core/src/main/java/hudson/tools/ZipExtractionInstaller.java
    @@ -42,9 +42,11 @@ import java.net.MalformedURLException;
     import java.net.URL;
     import java.net.URLConnection;
     
    +import jenkins.model.Jenkins;
     import org.jenkinsci.Symbol;
     import org.kohsuke.stapler.DataBoundConstructor;
     import org.kohsuke.stapler.QueryParameter;
    +import org.kohsuke.stapler.interceptor.RequirePOST;
     
     /**
      * Installs a tool into the Hudson working area by downloading and unpacking a ZIP file.
    @@ -95,7 +97,10 @@ public class ZipExtractionInstaller extends ToolInstaller {
                 return Messages.ZipExtractionInstaller_DescriptorImpl_displayName();
             }
     
    +        @RequirePOST
             public FormValidation doCheckUrl(@QueryParameter String value) {
    +            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
    +            
                 try {
                     URLConnection conn = ProxyConfiguration.open(new URL(value));
                     conn.connect();
    diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java
    index 0bacbc6e54eeb665b5699610bac417a4367b2a09..e45aa979c13a97a4c5a9ff0be192c2fcb80ec56c 100644
    --- a/core/src/main/java/hudson/triggers/SCMTrigger.java
    +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java
    @@ -37,6 +37,7 @@ import hudson.model.AdministrativeMonitor;
     import hudson.model.Cause;
     import hudson.model.CauseAction;
     import hudson.model.Item;
    +import hudson.model.PersistentDescriptor;
     import hudson.model.Run;
     import hudson.scm.SCM;
     import hudson.scm.SCMDescriptor;
    @@ -45,7 +46,7 @@ import hudson.util.FormValidation;
     import hudson.util.NamingThreadFactory;
     import hudson.util.SequentialExecutionQueue;
     import hudson.util.StreamTaskListener;
    -import hudson.util.TimeUnit2;
    +import java.util.concurrent.TimeUnit;
     import java.io.File;
     import java.io.IOException;
     import java.io.OutputStream;
    @@ -84,6 +85,8 @@ import org.kohsuke.stapler.QueryParameter;
     import org.kohsuke.stapler.StaplerRequest;
     import org.kohsuke.stapler.StaplerResponse;
     
    +import javax.annotation.PostConstruct;
    +
     import static java.util.logging.Level.WARNING;
     
     
    @@ -211,7 +214,7 @@ public class SCMTrigger extends Trigger<Item> {
         }
     
         @Extension @Symbol("pollSCM")
    -    public static class DescriptorImpl extends TriggerDescriptor {
    +    public static class DescriptorImpl extends TriggerDescriptor implements PersistentDescriptor {
     
             private static ThreadFactory threadFactory() {
                 return new NamingThreadFactory(Executors.defaultThreadFactory(), "SCMTrigger");
    @@ -235,13 +238,18 @@ public class SCMTrigger extends Trigger<Item> {
     
             /**
              * Max number of threads for SCM polling.
    -         * 0 for unbounded.
              */
    -        private int maximumThreads;
    +        private int maximumThreads = 10;
     
    -        public DescriptorImpl() {
    -            load();
    -            resizeThreadPool();
    +        private static final int THREADS_LOWER_BOUND = 5;
    +        private static final int THREADS_UPPER_BOUND = 100;
    +        private static final int THREADS_DEFAULT= 10;
    +
    +        private Object readResolve() {
    +            if (maximumThreads == 0) {
    +                maximumThreads = THREADS_DEFAULT;
    +            }
    +            return this;
             }
     
             public boolean isApplicable(Item item) {
    @@ -290,8 +298,6 @@ public class SCMTrigger extends Trigger<Item> {
             /**
              * Gets the number of concurrent threads used for polling.
              *
    -         * @return
    -         *      0 if unlimited.
              */
             public int getPollingThreadCount() {
                 return maximumThreads;
    @@ -299,12 +305,16 @@ public class SCMTrigger extends Trigger<Item> {
     
             /**
              * Sets the number of concurrent threads used for SCM polling and resizes the thread pool accordingly
    -         * @param n number of concurrent threads, zero or less means unlimited, maximum is 100
    +         * @param n number of concurrent threads in the range 5..100, outside values will set the to the nearest bound
              */
             public void setPollingThreadCount(int n) {
                 // fool proof
    -            if(n<0)     n=0;
    -            if(n>100)   n=100;
    +            if (n < THREADS_LOWER_BOUND) {
    +                n = THREADS_LOWER_BOUND;
    +            }
    +            if (n > THREADS_UPPER_BOUND) {
    +                n = THREADS_UPPER_BOUND;
    +            }
     
                 maximumThreads = n;
     
    @@ -313,7 +323,7 @@ public class SCMTrigger extends Trigger<Item> {
     
             @Restricted(NoExternalUse.class)
             public boolean isPollingThreadCountOptionVisible() {
    -            if (getPollingThreadCount() != 0) {
    +            if (getPollingThreadCount() != THREADS_DEFAULT) {
                     // this is a user who already configured the option
                     return true;
                 }
    @@ -336,18 +346,19 @@ public class SCMTrigger extends Trigger<Item> {
             /**
              * Update the {@link ExecutorService} instance.
              */
    +        @PostConstruct
             /*package*/ synchronized void resizeThreadPool() {
    -            queue.setExecutors(
    -                    (maximumThreads==0 ? Executors.newCachedThreadPool(threadFactory()) : Executors.newFixedThreadPool(maximumThreads, threadFactory())));
    +            queue.setExecutors(Executors.newFixedThreadPool(maximumThreads, threadFactory()));
             }
     
             @Override
             public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
                 String t = json.optString("pollingThreadCount",null);
    -            if(t==null || t.length()==0)
    -                setPollingThreadCount(0);
    -            else
    +            if (doCheckPollingThreadCount(t).kind != FormValidation.Kind.OK) {
    +                setPollingThreadCount(THREADS_DEFAULT);
    +            } else {
                     setPollingThreadCount(Integer.parseInt(t));
    +            }
     
                 // Save configuration
                 save();
    @@ -356,9 +367,7 @@ public class SCMTrigger extends Trigger<Item> {
             }
     
             public FormValidation doCheckPollingThreadCount(@QueryParameter String value) {
    -            if (value != null && "".equals(value.trim()))
    -                return FormValidation.ok();
    -            return FormValidation.validateNonNegativeInteger(value);
    +            return FormValidation.validateIntegerInRange(value, THREADS_LOWER_BOUND, THREADS_UPPER_BOUND);
             }
     
             /**
    @@ -462,7 +471,7 @@ public class SCMTrigger extends Trigger<Item> {
             }
             
             /**
    -         * Used from <tt>polling.jelly</tt> to write annotated polling log to the given output.
    +         * Used from {@code polling.jelly} to write annotated polling log to the given output.
              */
             public void writePollingLogTo(long offset, XMLOutput out) throws IOException {
                 // TODO: resurrect compressed log file support
    @@ -748,5 +757,5 @@ public class SCMTrigger extends Trigger<Item> {
         /**
          * How long is too long for a polling activity to be in the queue?
          */
    -    public static long STARVATION_THRESHOLD = SystemProperties.getLong(SCMTrigger.class.getName()+".starvationThreshold", TimeUnit2.HOURS.toMillis(1));
    +    public static long STARVATION_THRESHOLD = SystemProperties.getLong(SCMTrigger.class.getName()+".starvationThreshold", TimeUnit.HOURS.toMillis(1));
     }
    diff --git a/core/src/main/java/hudson/triggers/SafeTimerTask.java b/core/src/main/java/hudson/triggers/SafeTimerTask.java
    index e47dfc61e62b52872beaede1f3a03317cf5f5bfe..7e6f89236c74319e6d6475d01c318caf23fc72fb 100644
    --- a/core/src/main/java/hudson/triggers/SafeTimerTask.java
    +++ b/core/src/main/java/hudson/triggers/SafeTimerTask.java
    @@ -24,11 +24,18 @@
     package hudson.triggers;
     
     import hudson.model.AperiodicWork;
    +import hudson.model.AsyncAperiodicWork;
    +import hudson.model.AsyncPeriodicWork;
     import hudson.model.PeriodicWork;
     import hudson.security.ACL;
    +
    +import java.io.File;
     import java.util.TimerTask;
     import java.util.logging.Level;
     import java.util.logging.Logger;
    +
    +import jenkins.model.Jenkins;
    +import jenkins.util.SystemProperties;
     import jenkins.util.Timer;
     import org.acegisecurity.context.SecurityContext;
     import org.acegisecurity.context.SecurityContextHolder;
    @@ -43,6 +50,20 @@ import org.acegisecurity.context.SecurityContextHolder;
      * @since 1.124
      */
     public abstract class SafeTimerTask extends TimerTask {
    +
    +    /**
    +     * System property to change the location where (tasks) logging should be sent.
    +     * <p><strong>Beware: changing it while Jenkins is running gives no guarantee logs will be sent to the new location
    +     * until it is restarted.</strong></p>
    +     */
    +    static final String LOGS_ROOT_PATH_PROPERTY = SafeTimerTask.class.getName()+".logsTargetDir";
    +
    +    /**
    +     * Local marker to know if the information about using non default root directory for logs has already been logged at least once.
    +     * @see #LOGS_ROOT_PATH_PROPERTY
    +     */
    +    private static boolean ALREADY_LOGGED = false;
    +
         public final void run() {
             // background activity gets system credential,
             // just like executors get it.
    @@ -58,5 +79,31 @@ public abstract class SafeTimerTask extends TimerTask {
     
         protected abstract void doRun() throws Exception;
     
    +
    +    /**
    +     * The root path that should be used to put logs related to the tasks running in Jenkins.
    +     *
    +     * @see AsyncAperiodicWork#getLogFile()
    +     * @see AsyncPeriodicWork#getLogFile()
    +     * @return the path where the logs should be put.
    +     * @since 2.114
    +     */
    +    public static File getLogsRoot() {
    +        String tagsLogsPath = SystemProperties.getString(LOGS_ROOT_PATH_PROPERTY);
    +        if (tagsLogsPath == null) {
    +            return new File(Jenkins.get().getRootDir(), "logs");
    +        } else {
    +            Level logLevel = Level.INFO;
    +            if (ALREADY_LOGGED) {
    +                logLevel = Level.FINE;
    +            }
    +            LOGGER.log(logLevel,
    +                       "Using non default root path for tasks logging: {0}. (Beware: no automated migration if you change or remove it again)",
    +                       LOGS_ROOT_PATH_PROPERTY);
    +            ALREADY_LOGGED = true;
    +            return new File(tagsLogsPath);
    +        }
    +    }
    +
         private static final Logger LOGGER = Logger.getLogger(SafeTimerTask.class.getName());
     }
    diff --git a/core/src/main/java/hudson/triggers/package.html b/core/src/main/java/hudson/triggers/package.html
    index 61cb24e2f8c9e33741387c93eec0dc8471891c38..dd0f97ed8b139052e9766ec36ef5102be207faa3 100644
    --- a/core/src/main/java/hudson/triggers/package.html
    +++ b/core/src/main/java/hudson/triggers/package.html
    @@ -23,5 +23,5 @@ THE SOFTWARE.
     -->
     
     <html><head/><body>
    -Built-in <a href="Trigger.html"><tt>Trigger</tt></a>s that run periodically to kick a new build.
    +Built-in <a href="Trigger.html"><code>Trigger</code></a>s that run periodically to kick a new build.
     </body></html>
    \ No newline at end of file
    diff --git a/core/src/main/java/hudson/util/AbstractTaskListener.java b/core/src/main/java/hudson/util/AbstractTaskListener.java
    index 67372ed0160b934e10e9fefc8c44eb397a79f867..bb02e99cd000b8c4577e3eedd4502405cc720989 100644
    --- a/core/src/main/java/hudson/util/AbstractTaskListener.java
    +++ b/core/src/main/java/hudson/util/AbstractTaskListener.java
    @@ -1,17 +1,19 @@
     package hudson.util;
     
    -import hudson.console.HyperlinkNote;
    +import hudson.RestrictedSince;
     import hudson.model.TaskListener;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
    -import java.io.IOException;
     
     /**
    - * Partial default implementation of {@link TaskListener}
    - * @author Kohsuke Kawaguchi
    + * @deprecated implement {@link TaskListener} directly
      */
    +@Deprecated
    +@Restricted(NoExternalUse.class)
    +@RestrictedSince("2.91")
     public abstract class AbstractTaskListener implements TaskListener {
    -    public void hyperlink(String url, String text) throws IOException {
    -        annotate(new HyperlinkNote(url,text.length()));
    -        getLogger().print(text);
    -    }
    +
    +    private static final long serialVersionUID = 7217626701881006422L;
    +
     }
    diff --git a/core/src/main/java/hudson/util/ArgumentListBuilder.java b/core/src/main/java/hudson/util/ArgumentListBuilder.java
    index 87e2683587a436385917bc5f89df32d711c601d6..acc86c16d3804ac92d33a391cabb9d7d7a87028a 100644
    --- a/core/src/main/java/hudson/util/ArgumentListBuilder.java
    +++ b/core/src/main/java/hudson/util/ArgumentListBuilder.java
    @@ -38,6 +38,7 @@ import java.io.Serializable;
     import java.io.File;
     import java.io.IOException;
     import java.util.Set;
    +import javax.annotation.Nonnull;
     
     /**
      * Used to build up arguments for a process invocation.
    @@ -132,6 +133,16 @@ public class ArgumentListBuilder implements Serializable, Cloneable {
             }
             return this;
         }
    +    
    +    /**
    +     * @since 2.72
    +     */
    +    public ArgumentListBuilder add(@Nonnull Iterable<String> args) {
    +        for (String arg : args) {
    +            add(arg);
    +        }
    +        return this;
    +    }
     
         /**
          * Decomposes the given token into multiple arguments by splitting via whitespace.
    @@ -154,7 +165,7 @@ public class ArgumentListBuilder implements Serializable, Cloneable {
         /**
          * Adds key value pairs as "-Dkey=value -Dkey=value ..."
          *
    -     * <tt>-D</tt> portion is configurable as the 'prefix' parameter.
    +     * {@code -D} portion is configurable as the 'prefix' parameter.
          * @since 1.114
          */
         public ArgumentListBuilder addKeyValuePairs(String prefix, Map<String,String> props) {
    diff --git a/core/src/main/java/hudson/util/AtomicFileWriter.java b/core/src/main/java/hudson/util/AtomicFileWriter.java
    index e44f851dbab16f0f5bb0b8b4bed58e80649d0c36..a2fae13bac3221997772061e3aed4e23472df38c 100644
    --- a/core/src/main/java/hudson/util/AtomicFileWriter.java
    +++ b/core/src/main/java/hudson/util/AtomicFileWriter.java
    @@ -23,17 +23,23 @@
      */
     package hudson.util;
     
    -import hudson.Util;
    -import java.io.BufferedWriter;
    +import jenkins.util.SystemProperties;
    +
    +import javax.annotation.Nonnull;
    +import javax.annotation.Nullable;
     import java.io.File;
    -import java.io.FileOutputStream;
     import java.io.FileWriter;
     import java.io.IOException;
    -import java.io.OutputStreamWriter;
     import java.io.Writer;
     import java.nio.charset.Charset;
    +import java.nio.file.AtomicMoveNotSupportedException;
     import java.nio.file.Files;
     import java.nio.file.InvalidPathException;
    +import java.nio.file.Path;
    +import java.nio.file.StandardCopyOption;
    +import java.nio.file.StandardOpenOption;
    +import java.util.logging.Level;
    +import java.util.logging.Logger;
     
     /**
      * Buffered {@link FileWriter} that supports atomic operations.
    @@ -46,9 +52,20 @@ import java.nio.file.InvalidPathException;
      */
     public class AtomicFileWriter extends Writer {
     
    +    private static final Logger LOGGER = Logger.getLogger(AtomicFileWriter.class.getName());
    +
    +    private static /* final */ boolean DISABLE_FORCED_FLUSH = SystemProperties.getBoolean(
    +            AtomicFileWriter.class.getName() + ".DISABLE_FORCED_FLUSH");
    +
    +    static {
    +        if (DISABLE_FORCED_FLUSH) {
    +            LOGGER.log(Level.WARNING, "DISABLE_FORCED_FLUSH flag used, this could result in dataloss if failures happen in your storage subsystem.");
    +        }
    +    }
    +
         private final Writer core;
    -    private final File tmpFile;
    -    private final File destFile;
    +    private final Path tmpPath;
    +    private final Path destPath;
     
         /**
          * Writes with UTF-8 encoding.
    @@ -58,25 +75,81 @@ public class AtomicFileWriter extends Writer {
         }
     
         /**
    -     * @param encoding
    -     *      File encoding to write. If null, platform default encoding is chosen.
    +     * @param encoding File encoding to write. If null, platform default encoding is chosen.
    +     *
    +     * @deprecated Use {@link #AtomicFileWriter(Path, Charset)}
    +     */
    +    @Deprecated
    +    public AtomicFileWriter(@Nonnull File f, @Nullable String encoding) throws IOException {
    +        this(toPath(f), encoding == null ? Charset.defaultCharset() : Charset.forName(encoding));
    +    }
    +
    +    /**
    +     * Wraps potential {@link java.nio.file.InvalidPathException} thrown by {@link File#toPath()} in an
    +     * {@link IOException} for backward compatibility.
    +     *
    +     * @param file
    +     * @return the path for that file
    +     * @see File#toPath()
    +     */
    +    private static Path toPath(@Nonnull File file) throws IOException {
    +        try {
    +            return file.toPath();
    +        } catch (InvalidPathException e) {
    +            throw new IOException(e);
    +        }
    +    }
    +
    +    /**
    +     * @param destinationPath the destination path where to write the content when committed.
    +     * @param charset File charset to write.
          */
    -    public AtomicFileWriter(File f, String encoding) throws IOException {
    -        File dir = f.getParentFile();
    +    public AtomicFileWriter(@Nonnull Path destinationPath, @Nonnull Charset charset) throws IOException {
    +        // See FileChannelWriter docs to understand why we do not cause a force() call on flush() from AtomicFileWriter.
    +        this(destinationPath, charset, false, true);
    +    }
    +
    +    /**
    +     * <strong>DO NOT USE THIS METHOD, OR YOU WILL LOSE DATA INTEGRITY.</strong>
    +     *
    +     * @param destinationPath the destination path where to write the content when committed.
    +     * @param charset File charset to write.
    +     * @param integrityOnFlush do not force writing to disk when flushing
    +     * @param integrityOnClose do not force writing to disk when closing
    +     * @deprecated use {@link AtomicFileWriter#AtomicFileWriter(Path, Charset)}
    +     */
    +    @Deprecated
    +    public AtomicFileWriter(@Nonnull Path destinationPath, @Nonnull Charset charset, boolean integrityOnFlush, boolean integrityOnClose) throws IOException {
    +        if (charset == null) { // be extra-defensive if people don't care
    +            throw new IllegalArgumentException("charset is null");
    +        }
    +        this.destPath = destinationPath;
    +        Path dir = this.destPath.getParent();
    +
    +        if (Files.exists(dir) && !Files.isDirectory(dir)) {
    +            throw new IOException(dir + " exists and is neither a directory nor a symlink to a directory");
    +        }
    +        else {
    +            if (Files.isSymbolicLink(dir)) {
    +                LOGGER.log(Level.CONFIG, "{0} is a symlink to a directory", dir);
    +            } else {
    +                Files.createDirectories(dir); // Cannot be called on symlink, so we are pretty defensive...
    +            }
    +        }
    +
             try {
    -            dir.mkdirs();
    -            tmpFile = File.createTempFile("atomic",null, dir);
    +            // JENKINS-48407: NIO's createTempFile creates file with 0600 permissions, so we use pre-NIO for this...
    +            tmpPath = File.createTempFile("atomic", "tmp", dir.toFile()).toPath();
             } catch (IOException e) {
                 throw new IOException("Failed to create a temporary file in "+ dir,e);
             }
    -        destFile = f;
    -        if (encoding==null)
    -            encoding = Charset.defaultCharset().name();
    -        try {
    -            core = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(tmpFile.toPath()), encoding));
    -        } catch (InvalidPathException e) {
    -            throw new IOException(e);
    +
    +        if (DISABLE_FORCED_FLUSH) {
    +            integrityOnFlush = false;
    +            integrityOnClose = false;
             }
    +
    +        core = new FileChannelWriter(tmpPath, charset, integrityOnFlush, integrityOnClose, StandardOpenOption.WRITE);
         }
     
         @Override
    @@ -108,35 +181,77 @@ public class AtomicFileWriter extends Writer {
          * the {@link #commit()} is called, to simplify coding.
          */
         public void abort() throws IOException {
    -        close();
    -        tmpFile.delete();
    +        closeAndDeleteTempFile();
         }
     
         public void commit() throws IOException {
             close();
    -        if (destFile.exists()) {
    +        try {
    +            // Try to make an atomic move.
    +            Files.move(tmpPath, destPath, StandardCopyOption.ATOMIC_MOVE);
    +        } catch (IOException e) {
    +            // If it falls here that can mean many things. Either that the atomic move is not supported,
    +            // or something wrong happened. Anyway, let's try to be over-diagnosing
    +            if (e instanceof AtomicMoveNotSupportedException) {
    +                LOGGER.log(Level.WARNING, "Atomic move not supported. falling back to non-atomic move.", e);
    +            } else {
    +                LOGGER.log(Level.WARNING, "Unable to move atomically, falling back to non-atomic move.", e);
    +            }
    +
    +            if (destPath.toFile().exists()) {
    +                LOGGER.log(Level.INFO, "The target file {0} was already existing", destPath);
    +            }
    +
                 try {
    -                Util.deleteFile(destFile);
    -            } catch (IOException x) {
    -                tmpFile.delete();
    -                throw x;
    +                Files.move(tmpPath, destPath, StandardCopyOption.REPLACE_EXISTING);
    +            } catch (IOException e1) {
    +                e1.addSuppressed(e);
    +                LOGGER.log(Level.WARNING, "Unable to move {0} to {1}. Attempting to delete {0} and abandoning.",
    +                           new Path[]{tmpPath, destPath});
    +                try {
    +                    Files.deleteIfExists(tmpPath);
    +                } catch (IOException e2) {
    +                    e2.addSuppressed(e1);
    +                    LOGGER.log(Level.WARNING, "Unable to delete {0}, good bye then!", tmpPath);
    +                    throw e2;
    +                }
    +
    +                throw e1;
                 }
             }
    -        tmpFile.renameTo(destFile);
         }
     
         @Override
         protected void finalize() throws Throwable {
    +        closeAndDeleteTempFile();
    +    }
    +
    +    private void closeAndDeleteTempFile() throws IOException {
             // one way or the other, temporary file should be deleted.
    -        close();
    -        tmpFile.delete();
    +        try {
    +            close();
    +        } finally {
    +            Files.deleteIfExists(tmpPath);
    +        }
         }
     
         /**
          * Until the data is committed, this file captures
          * the written content.
    +     *
    +     * @deprecated Use getTemporaryPath()
          */
    +    @Deprecated
         public File getTemporaryFile() {
    -        return tmpFile;
    +        return tmpPath.toFile();
    +    }
    +
    +    /**
    +     * Until the data is committed, this file captures
    +     * the written content.
    +     * @since 2.93
    +     */
    +    public Path getTemporaryPath() {
    +        return tmpPath;
         }
     }
    diff --git a/core/src/main/java/hudson/util/ClassLoaderSanityThreadFactory.java b/core/src/main/java/hudson/util/ClassLoaderSanityThreadFactory.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..2977e9bfbd45f5588f180b3cc3d62f18312cfad9
    --- /dev/null
    +++ b/core/src/main/java/hudson/util/ClassLoaderSanityThreadFactory.java
    @@ -0,0 +1,27 @@
    +package hudson.util;
    +
    +import java.util.concurrent.ThreadFactory;
    +import java.util.concurrent.TimeUnit;
    +
    +/**
    + *  Explicitly sets the {@link Thread#contextClassLoader} for threads it creates to its own classloader.
    + *  This avoids issues where threads are lazily created (ex by invoking {@link java.util.concurrent.ScheduledExecutorService#schedule(Runnable, long, TimeUnit)})
    + *   in a context where they would receive a customized {@link Thread#contextClassLoader} that was never meant to be used.
    + *
    + *  Commonly this is a problem for Groovy use, where this may result in memory leaks.
    + *  @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-49206">JENKINS-49206</a>
    + * @since 2.105
    + */
    +public class ClassLoaderSanityThreadFactory implements ThreadFactory {
    +    private final ThreadFactory delegate;
    +
    +    public ClassLoaderSanityThreadFactory(ThreadFactory delegate) {
    +        this.delegate = delegate;
    +    }
    +
    +    @Override public Thread newThread(Runnable r) {
    +        Thread t = delegate.newThread(r);
    +        t.setContextClassLoader(ClassLoaderSanityThreadFactory.class.getClassLoader());
    +        return t;
    +    }
    +}
    diff --git a/core/src/main/java/hudson/util/DescribableList.java b/core/src/main/java/hudson/util/DescribableList.java
    index 7d90a2ed745a3643b553adbe742353a898ae6b38..18642093c7f4e1fc1a013274797907c9e0ff25d8 100644
    --- a/core/src/main/java/hudson/util/DescribableList.java
    +++ b/core/src/main/java/hudson/util/DescribableList.java
    @@ -270,10 +270,9 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
             }
     
             public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    -            CopyOnWriteList core = copyOnWriteListConverter.unmarshal(reader, context);
    -
                 try {
    -                DescribableList r = (DescribableList)context.getRequiredType().newInstance();
    +                DescribableList r = (DescribableList) context.getRequiredType().asSubclass(DescribableList.class).newInstance();
    +                CopyOnWriteList core = copyOnWriteListConverter.unmarshal(reader, context);
                     r.data.replaceBy(core);
                     return r;
                 } catch (InstantiationException e) {
    diff --git a/core/src/main/java/hudson/util/DirScanner.java b/core/src/main/java/hudson/util/DirScanner.java
    index a694c7c21b820db408775953416c3b3b6535e133..2551aa0b081a0d6512be0e44d745de9063e60e35 100644
    --- a/core/src/main/java/hudson/util/DirScanner.java
    +++ b/core/src/main/java/hudson/util/DirScanner.java
    @@ -7,7 +7,6 @@ import org.apache.tools.ant.types.FileSet;
     import java.io.File;
     import java.io.FileFilter;
     import java.io.IOException;
    -import java.io.InterruptedIOException;
     import java.io.Serializable;
     
     import static hudson.Util.fixEmpty;
    @@ -31,19 +30,15 @@ public abstract class DirScanner implements Serializable {
          */
         protected final void scanSingle(File f, String relative, FileVisitor visitor) throws IOException {
             if (visitor.understandsSymlink()) {
    +            String target;
                 try {
    -                String target;
    -                try {
    -                    target = Util.resolveSymlink(f);
    -                } catch (IOException x) { // JENKINS-13202
    -                    target = null;
    -                }
    -                if (target != null) {
    -                    visitor.visitSymlink(f, target, relative);
    -                    return;
    -                }
    -            } catch (InterruptedException e) {
    -                throw (IOException) new InterruptedIOException().initCause(e);
    +                target = Util.resolveSymlink(f);
    +            } catch (IOException x) { // JENKINS-13202
    +                target = null;
    +            }
    +            if (target != null) {
    +                visitor.visitSymlink(f, target, relative);
    +                return;
                 }
             }
             visitor.visit(f, relative);
    diff --git a/core/src/main/java/hudson/util/DoubleLaunchChecker.java b/core/src/main/java/hudson/util/DoubleLaunchChecker.java
    index 8bbcdcab08e32bea00469c84195a7772c2d54ae2..6fff568d048914c3657d4b329d22edb8965a927d 100644
    --- a/core/src/main/java/hudson/util/DoubleLaunchChecker.java
    +++ b/core/src/main/java/hudson/util/DoubleLaunchChecker.java
    @@ -47,7 +47,7 @@ import java.lang.management.ManagementFactory;
     import java.lang.reflect.Method;
     
     /**
    - * Makes sure that no other Hudson uses our <tt>JENKINS_HOME</tt> directory,
    + * Makes sure that no other Hudson uses our {@code JENKINS_HOME} directory,
      * to forestall the problem of running multiple instances of Hudson that point to the same data directory.
      *
      * <p>
    diff --git a/core/src/main/java/hudson/util/ErrorObject.java b/core/src/main/java/hudson/util/ErrorObject.java
    index def3df9adb9b8b2357ad25ebc69448ef28f9e622..5d4a8593570c0b549b5a364a5bac25e0b5011e48 100644
    --- a/core/src/main/java/hudson/util/ErrorObject.java
    +++ b/core/src/main/java/hudson/util/ErrorObject.java
    @@ -35,7 +35,7 @@ import java.io.IOException;
      * Basis for error model objects.
      *
      * This implementation serves error pages for any requests under its domain. Subclasses are responsible for providing
    - * <tt>index</tt> view.
    + * {@code index} view.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/FileChannelWriter.java b/core/src/main/java/hudson/util/FileChannelWriter.java
    new file mode 100644
    index 0000000000000000000000000000000000000000..8d623740d1492dce9e50268dc5ba93bacae9137d
    --- /dev/null
    +++ b/core/src/main/java/hudson/util/FileChannelWriter.java
    @@ -0,0 +1,94 @@
    +package hudson.util;
    +
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
    +
    +import java.io.IOException;
    +import java.io.Writer;
    +import java.nio.ByteBuffer;
    +import java.nio.CharBuffer;
    +import java.nio.channels.FileChannel;
    +import java.nio.charset.Charset;
    +import java.nio.file.OpenOption;
    +import java.nio.file.Path;
    +import java.util.logging.Logger;
    +
    +/**
    + * This class has been created to help make {@link AtomicFileWriter} hopefully more reliable in some corner cases.
    + * We created this wrapper to be able to access {@link FileChannel#force(boolean)} which seems to be one of the rare
    + * ways to actually have a guarantee that data be flushed to the physical device (only guaranteed for local, not for
    + * remote obviously though).
    + *
    + * <p>The goal using this is to reduce as much as we can the likeliness to see zero-length files be created in place
    + * of the original ones.</p>
    + *
    + * @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-34855">JENKINS-34855</a>
    + * @see <a href="https://github.com/jenkinsci/jenkins/pull/2548">PR-2548</a>
    + */
    +@Restricted(NoExternalUse.class)
    +public class FileChannelWriter extends Writer {
    +
    +    private static final Logger LOGGER = Logger.getLogger(FileChannelWriter.class.getName());
    +
    +    private final Charset charset;
    +    private final FileChannel channel;
    +
    +    /**
    +     * {@link FileChannel#force(boolean)} is a <strong>very</strong> costly operation. This flag has been introduced mostly to
    +     * accommodate Jenkins' previous behaviour, when using a simple {@link java.io.BufferedWriter}.
    +     *
    +     * <p>Basically, {@link BufferedWriter#flush()} does nothing, so when existing code was rewired to use
    +     * {@link FileChannelWriter#flush()} behind {@link AtomicFileWriter} and that method actually ends up calling
    +     * {@link FileChannel#force(boolean)}, many things started timing out. The main reason is probably because XStream's
    +     * {@link com.thoughtworks.xstream.core.util.QuickWriter} uses <code>flush()</code> a lot.
    +     * So we introduced this field to be able to still get a better integrity for the use case of {@link AtomicFileWriter}.
    +     * Because from there, we make sure to call {@link #close()} from {@link AtomicFileWriter#commit()} anyway.
    +     */
    +    private boolean forceOnFlush;
    +
    +    /**
    +     * See forceOnFlush. You probably never want to set forceOnClose to false.
    +     */
    +    private boolean forceOnClose;
    +
    +    /**
    +     * @param filePath     the path of the file to write to.
    +     * @param charset      the charset to use when writing characters.
    +     * @param forceOnFlush set to true if you want {@link FileChannel#force(boolean)} to be called on {@link #flush()}.
    +     * @param options      the options for opening the file.
    +     * @throws IOException if something went wrong.
    +     */
    +    FileChannelWriter(Path filePath, Charset charset, boolean forceOnFlush, boolean forceOnClose, OpenOption... options) throws IOException {
    +        this.charset = charset;
    +        this.forceOnFlush = forceOnFlush;
    +        this.forceOnClose = forceOnClose;
    +        channel = FileChannel.open(filePath, options);
    +    }
    +
    +    @Override
    +    public void write(char cbuf[], int off, int len) throws IOException {
    +        final CharBuffer charBuffer = CharBuffer.wrap(cbuf, off, len);
    +        ByteBuffer byteBuffer = charset.encode(charBuffer);
    +        channel.write(byteBuffer);
    +    }
    +
    +    @Override
    +    public void flush() throws IOException {
    +        if (forceOnFlush) {
    +            LOGGER.finest("Flush is forced");
    +            channel.force(true);
    +        } else {
    +            LOGGER.finest("Force disabled on flush(), no-op");
    +        }
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        if(channel.isOpen()) {
    +            if (forceOnClose) {
    +                channel.force(true);
    +            }
    +            channel.close();
    +        }
    +    }
    +}
    diff --git a/core/src/main/java/hudson/util/FormFieldValidator.java b/core/src/main/java/hudson/util/FormFieldValidator.java
    index 47ba955121b9daf1f2b29e0d0e42103e040fec74..0595afa4a68a80f12ec60235467f3923f7109430 100644
    --- a/core/src/main/java/hudson/util/FormFieldValidator.java
    +++ b/core/src/main/java/hudson/util/FormFieldValidator.java
    @@ -170,8 +170,8 @@ public abstract class FormFieldValidator {
          * Sends out a string error message that indicates an error.
          *
          * @param message
    -     *      Human readable message to be sent. <tt>error(null)</tt>
    -     *      can be used as <tt>ok()</tt>.
    +     *      Human readable message to be sent. {@code error(null)}
    +     *      can be used as {@code ok()}.
          */
         public void error(String message) throws IOException, ServletException {
             errorWithMarkup(message==null?null:Util.escape(message));
    @@ -209,8 +209,8 @@ public abstract class FormFieldValidator {
          * attack.
          *
          * @param message
    -     *      Human readable message to be sent. <tt>error(null)</tt>
    -     *      can be used as <tt>ok()</tt>.
    +     *      Human readable message to be sent. {@code error(null)}
    +     *      can be used as {@code ok()}.
          */
         public void errorWithMarkup(String message) throws IOException, ServletException {
             _errorWithMarkup(message,"error");
    diff --git a/core/src/main/java/hudson/util/FormFillFailure.java b/core/src/main/java/hudson/util/FormFillFailure.java
    index 8fcf650b7ff9cdd183169a8d7bf04052faade015..16b8e6435a2fc918fe48a731d327694c49d16f68 100644
    --- a/core/src/main/java/hudson/util/FormFillFailure.java
    +++ b/core/src/main/java/hudson/util/FormFillFailure.java
    @@ -40,7 +40,7 @@ import org.kohsuke.stapler.StaplerResponse;
      * Represents a failure in a form field doFillXYZ method.
      *
      * <p>
    - * Use one of the factory methods to create an instance, then throw it from your <tt>doFillXyz</tt>
    + * Use one of the factory methods to create an instance, then throw it from your {@code doFillXyz}
      * method.
      *
      * @since 2.50
    @@ -117,8 +117,8 @@ public abstract class FormFillFailure extends IOException implements HttpRespons
          * This method must be used with care to avoid cross-site scripting
          * attack.
          *
    -     * @param message Human readable message to be sent. <tt>error(null)</tt>
    -     *                can be used as <tt>ok()</tt>.
    +     * @param message Human readable message to be sent. {@code error(null)}
    +     *                can be used as {@code ok()}.
          */
         public static FormFillFailure errorWithMarkup(String message) {
             return _errorWithMarkup(message, FormValidation.Kind.ERROR);
    diff --git a/core/src/main/java/hudson/util/FormValidation.java b/core/src/main/java/hudson/util/FormValidation.java
    index c95e5d1da81cd3a17c6b5d69f28626429c4b4eeb..90748cf224df2774fcf99d9a8a553f109072a529 100644
    --- a/core/src/main/java/hudson/util/FormValidation.java
    +++ b/core/src/main/java/hudson/util/FormValidation.java
    @@ -66,7 +66,7 @@ import static hudson.Util.*;
      * Represents the result of the form field validation.
      *
      * <p>
    - * Use one of the factory methods to create an instance, then return it from your <tt>doCheckXyz</tt>
    + * Use one of the factory methods to create an instance, then return it from your {@code doCheckXyz}
      * method. (Via {@link HttpResponse}, the returned object will render the result into {@link StaplerResponse}.)
      * This way of designing form field validation allows you to reuse {@code doCheckXyz()} methods
      * programmatically as well (by using {@link #kind}.
    @@ -77,7 +77,7 @@ import static hudson.Util.*;
      * that you may be able to reuse.
      *
      * <p>
    - * Also see <tt>doCheckCvsRoot</tt> in <tt>CVSSCM</tt> as an example.
    + * Also see {@code doCheckCvsRoot} in {@code CVSSCM} as an example.
      *
      * <p>
      * This class extends {@link IOException} so that it can be thrown from a method. This allows one to reuse
    @@ -136,8 +136,8 @@ public abstract class FormValidation extends IOException implements HttpResponse
          * Sends out a string error message that indicates an error.
          *
          * @param message
    -     *      Human readable message to be sent. <tt>error(null)</tt>
    -     *      can be used as <tt>ok()</tt>.
    +     *      Human readable message to be sent. {@code error(null)}
    +     *      can be used as {@code ok()}.
          */
         public static FormValidation error(String message) {
             return errorWithMarkup(message==null?null: Util.escape(message));
    @@ -245,8 +245,8 @@ public abstract class FormValidation extends IOException implements HttpResponse
          * attack.
          *
          * @param message
    -     *      Human readable message to be sent. <tt>error(null)</tt>
    -     *      can be used as <tt>ok()</tt>.
    +     *      Human readable message to be sent. {@code error(null)}
    +     *      can be used as {@code ok()}.
          */
         public static FormValidation errorWithMarkup(String message) {
             return _errorWithMarkup(message,Kind.ERROR);
    @@ -393,6 +393,30 @@ public abstract class FormValidation extends IOException implements HttpResponse
             }
         }
     
    +    /**
    +     * Make sure that the given string is an integer in the range specified by the lower and upper bounds (both inclusive)
    +     *
    +     * @param value the value to check
    +     * @param lower the lower bound (inclusive)
    +     * @param upper the upper bound (inclusive)
    +     *
    +     * @since 2.104
    +     */
    +    public static FormValidation validateIntegerInRange(String value, int lower, int upper) {
    +        try {
    +            int intValue = Integer.parseInt(value);
    +            if (intValue < lower) {
    +                return error(hudson.model.Messages.Hudson_MustBeAtLeast(lower));
    +            }
    +            if (intValue > upper) {
    +                return error(hudson.model.Messages.Hudson_MustBeAtMost(upper));
    +            }
    +            return ok();
    +        } catch (NumberFormatException e) {
    +            return error(hudson.model.Messages.Hudson_NotANumber());
    +        }
    +    }
    +
         /**
          * Makes sure that the given string is a positive integer.
          */
    diff --git a/core/src/main/java/hudson/util/Function1.java b/core/src/main/java/hudson/util/Function1.java
    index c200c28c3a2c7fcee3a829fe7d225eb028c32900..aa7535d5903f75a572c99f39139e526567b7fc17 100644
    --- a/core/src/main/java/hudson/util/Function1.java
    +++ b/core/src/main/java/hudson/util/Function1.java
    @@ -24,7 +24,7 @@
     package hudson.util;
     
     /**
    - * Unary function <tt>y=f(x)</tt>.
    + * Unary function {@code y=f(x)}.
      * 
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/HistoricalSecrets.java b/core/src/main/java/hudson/util/HistoricalSecrets.java
    index 37a6fa39e170b3a3a236f7576253205b6d82de0e..962ab260896c761857b7d8647667eb1a79a66f5f 100644
    --- a/core/src/main/java/hudson/util/HistoricalSecrets.java
    +++ b/core/src/main/java/hudson/util/HistoricalSecrets.java
    @@ -80,5 +80,5 @@ public class HistoricalSecrets {
             return Util.toAes128Key(secret);
         }
     
    -    private static final String MAGIC = "::::MAGIC::::";
    +    static final String MAGIC = "::::MAGIC::::";
     }
    diff --git a/core/src/main/java/hudson/util/HttpResponses.java b/core/src/main/java/hudson/util/HttpResponses.java
    index d6b8fb225700ee5860b869d8a5cca84e13b95643..ef731f3ff8252407ef48beeebe6109c2214ae32d 100644
    --- a/core/src/main/java/hudson/util/HttpResponses.java
    +++ b/core/src/main/java/hudson/util/HttpResponses.java
    @@ -89,16 +89,52 @@ public class HttpResponses extends org.kohsuke.stapler.HttpResponses {
             return new JSONObjectResponse(data);
         }
     
    -        /**
    -         * Set the response as an error response.
    -         * @param message The error "message" set on the response.
    -         * @return {@code this} object.
    -         *
    -         * @since 2.0
    -         */
    +    /**
    +     * Set the response as an error response.
    +     * @param message The error "message" set on the response.
    +     * @return {@code this} object.
    +     *
    +     * @since 2.0
    +     */
         public static HttpResponse errorJSON(@Nonnull String message) {
             return new JSONObjectResponse().error(message);
         }
    +    
    +    /**
    +     * Set the response as an error response plus some data.
    +     * @param message The error "message" set on the response.
    +     * @param data The data.
    +     * @return {@code this} object.
    +     *
    +     * @since 2.119
    +     */
    +    public static HttpResponse errorJSON(@Nonnull String message, @Nonnull Map<?,?> data) {
    +        return new JSONObjectResponse(data).error(message);
    +    }
    +
    +    /**
    +     * Set the response as an error response plus some data.
    +     * @param message The error "message" set on the response.
    +     * @param data The data.
    +     * @return {@code this} object.
    +     *
    +     * @since 2.115
    +     */
    +    public static HttpResponse errorJSON(@Nonnull String message, @Nonnull JSONObject data) {
    +        return new JSONObjectResponse(data).error(message);
    +    }
    +
    +    /**
    +     * Set the response as an error response plus some data.
    +     * @param message The error "message" set on the response.
    +     * @param data The data.
    +     * @return {@code this} object.
    +     *
    +     * @since 2.115
    +     */
    +    public static HttpResponse errorJSON(@Nonnull String message, @Nonnull JSONArray data) {
    +        return new JSONObjectResponse(data).error(message);
    +    }
     
         /**
          * {@link net.sf.json.JSONObject} response.
    diff --git a/core/src/main/java/hudson/util/HudsonFailedToLoad.java b/core/src/main/java/hudson/util/HudsonFailedToLoad.java
    index 72ad0a4f9c629508703a772b9e56885600958496..8347c7a1cf3f5a3e83d795236a634876c72755f9 100644
    --- a/core/src/main/java/hudson/util/HudsonFailedToLoad.java
    +++ b/core/src/main/java/hudson/util/HudsonFailedToLoad.java
    @@ -30,7 +30,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
      * Model object used to display the generic error when Jenkins start up fails fatally during initialization.
      *
      * <p>
    - * <tt>index.jelly</tt> would display a nice friendly error page.
    + * {@code index.jelly} would display a nice friendly error page.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/IOUtils.java b/core/src/main/java/hudson/util/IOUtils.java
    index d726b43df50954c4ebd72c5579335bfc9531fe4e..2bcdf90acbdcea173c3b8f82db7ea0a1cf71f07d 100644
    --- a/core/src/main/java/hudson/util/IOUtils.java
    +++ b/core/src/main/java/hudson/util/IOUtils.java
    @@ -1,6 +1,7 @@
     package hudson.util;
     
     import hudson.Functions;
    +import hudson.Util;
     import hudson.os.PosixAPI;
     import hudson.os.PosixException;
     import java.nio.file.Files;
    @@ -12,6 +13,8 @@ import java.util.Collection;
     import java.util.List;
     import java.util.regex.Pattern;
     
    +import static hudson.Util.fileToPath;
    +
     /**
      * Adds more to commons-io.
      *
    @@ -50,20 +53,11 @@ public class IOUtils {
          *      This method returns the 'dir' parameter so that the method call flows better.
          */
         public static File mkdirs(File dir) throws IOException {
    -        if(dir.mkdirs() || dir.exists())
    -            return dir;
    -
    -        // following Ant <mkdir> task to avoid possible race condition.
             try {
    -            Thread.sleep(10);
    -        } catch (InterruptedException e) {
    -            // ignore
    +            return Files.createDirectories(fileToPath(dir)).toFile();
    +        } catch (UnsupportedOperationException e) {
    +            throw new IOException(e);
             }
    -
    -        if (dir.mkdirs() || dir.exists())
    -            return dir;
    -
    -        throw new IOException("Failed to create a directory at "+dir);
         }
     
         /**
    @@ -119,13 +113,27 @@ public class IOUtils {
     
     
         /**
    -     * Gets the mode of a file/directory, if appropriate.
    +     * Gets the mode of a file/directory, if appropriate. Only includes read, write, and
    +     * execute permissions for the owner, group, and others, i.e. the max return value
    +     * is 0777. Consider using {@link Files#getPosixFilePermissions} instead if you only
    +     * care about access permissions.
    +     * <p>If the file is symlink, the mode is that of the link target, not the link itself.
          * @return a file mode, or -1 if not on Unix
          * @throws PosixException if the file could not be statted, e.g. broken symlink
          */
         public static int mode(File f) throws PosixException {
             if(Functions.isWindows())   return -1;
    -        return PosixAPI.jnr().stat(f.getPath()).mode();
    +        try {
    +            if (Util.NATIVE_CHMOD_MODE) {
    +                return PosixAPI.jnr().stat(f.getPath()).mode();
    +            } else {
    +                return Util.permissionsToMode(Files.getPosixFilePermissions(fileToPath(f)));
    +            }
    +        } catch (IOException cause) {
    +            PosixException e = new PosixException("Unable to get file permissions", null);
    +            e.initCause(cause);
    +            throw e;
    +        }
         }
     
         /**
    diff --git a/core/src/main/java/hudson/util/IncompatibleAntVersionDetected.java b/core/src/main/java/hudson/util/IncompatibleAntVersionDetected.java
    index 1a89b82e6f2f877759f9a8135667ffbf16ea7ddb..c3d34afd4edfbea54d0a2bfe71a5f20b4c297152 100644
    --- a/core/src/main/java/hudson/util/IncompatibleAntVersionDetected.java
    +++ b/core/src/main/java/hudson/util/IncompatibleAntVersionDetected.java
    @@ -33,7 +33,7 @@ import java.net.URL;
      * we find out that the container is picking up its own Ant and that's not 1.7.
      *
      * <p>
    - * <tt>index.jelly</tt> would display a nice friendly error page.
    + * {@code index.jelly} would display a nice friendly error page.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/IncompatibleServletVersionDetected.java b/core/src/main/java/hudson/util/IncompatibleServletVersionDetected.java
    index 92d17d185df650cfbdccf593b35b58bd4c67a6f8..f2e4931d6ab9265a0f3dae05a056953a03dfa3f4 100644
    --- a/core/src/main/java/hudson/util/IncompatibleServletVersionDetected.java
    +++ b/core/src/main/java/hudson/util/IncompatibleServletVersionDetected.java
    @@ -33,7 +33,7 @@ import java.net.URL;
      * we find out that the container doesn't support servlet 2.4.
      *
      * <p>
    - * <tt>index.jelly</tt> would display a nice friendly error page.
    + * {@code index.jelly} would display a nice friendly error page.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/IncompatibleVMDetected.java b/core/src/main/java/hudson/util/IncompatibleVMDetected.java
    index ae690c162bb77dc14ca31b2deeaad13e6bcd71b9..651e5b124950c33efb76cc85e8f057c646df5008 100644
    --- a/core/src/main/java/hudson/util/IncompatibleVMDetected.java
    +++ b/core/src/main/java/hudson/util/IncompatibleVMDetected.java
    @@ -30,7 +30,7 @@ import java.util.Map;
      * we find out that XStream is running in pure-java mode.
      *
      * <p>
    - * <tt>index.jelly</tt> would display a nice friendly error page.
    + * {@code index.jelly} would display a nice friendly error page.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/InsufficientPermissionDetected.java b/core/src/main/java/hudson/util/InsufficientPermissionDetected.java
    index 739e930bb9fed64598d22773c8e496f40e848406..57aea00f22e6479abe2cf45f23b1d0c91695eb15 100644
    --- a/core/src/main/java/hudson/util/InsufficientPermissionDetected.java
    +++ b/core/src/main/java/hudson/util/InsufficientPermissionDetected.java
    @@ -31,7 +31,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
      * we find that we don't have enough permissions to run.
      *
      * <p>
    - * <tt>index.jelly</tt> would display a nice friendly error page.
    + * {@code index.jelly} would display a nice friendly error page.
      *
      * @author Kohsuke Kawaguchi
      */
    diff --git a/core/src/main/java/hudson/util/Iterators.java b/core/src/main/java/hudson/util/Iterators.java
    index 74d82950dc298e8d79b0ccc471dd95d340e12163..8d93d7226212ef01aba818caf5beb60050914ffb 100644
    --- a/core/src/main/java/hudson/util/Iterators.java
    +++ b/core/src/main/java/hudson/util/Iterators.java
    @@ -23,6 +23,7 @@
      */
     package hudson.util;
     
    +import com.google.common.annotations.Beta;
     import com.google.common.base.Predicates;
     import com.google.common.collect.ImmutableList;
     
    @@ -35,6 +36,9 @@ import java.util.AbstractList;
     import java.util.Arrays;
     import java.util.Set;
     import java.util.HashSet;
    +import javax.annotation.Nonnull;
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
      * Varios {@link Iterator} implementations.
    @@ -403,4 +407,20 @@ public class Iterators {
         public interface CountingPredicate<T> {
             boolean apply(int index, T input);
         }
    +
    +    /**
    +     * Similar to {@link com.google.common.collect.Iterators#skip} except not {@link Beta}.
    +     * @param iterator some iterator
    +     * @param count a nonnegative count
    +     */
    +    @Restricted(NoExternalUse.class)
    +    public static void skip(@Nonnull Iterator<?> iterator, int count) {
    +        if (count < 0) {
    +            throw new IllegalArgumentException();
    +        }
    +        while (iterator.hasNext() && count-- > 0) {
    +            iterator.next();
    +        }
    +    }
    +
     }
    diff --git a/core/src/main/java/hudson/util/JenkinsReloadFailed.java b/core/src/main/java/hudson/util/JenkinsReloadFailed.java
    index 7022844bce4a54b02254e0f53fc02f6a39489e61..ab27d5969f29c657dd4cba28e639f5f0e61fc044 100644
    --- a/core/src/main/java/hudson/util/JenkinsReloadFailed.java
    +++ b/core/src/main/java/hudson/util/JenkinsReloadFailed.java
    @@ -9,7 +9,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
      * @author Kohsuke Kawaguchi
      */
     public class JenkinsReloadFailed extends BootFailure {
    -    @Restricted(NoExternalUse.class) @Deprecated
    +    @Restricted(NoExternalUse.class)
         public final Throwable cause;
     
         public JenkinsReloadFailed(Throwable cause) {
    diff --git a/core/src/main/java/hudson/util/KeyedDataStorage.java b/core/src/main/java/hudson/util/KeyedDataStorage.java
    index 49a220b1b245db200a356c90f7bb0c41ea7ed986..18344394551520136eeffd6bc165ac9b9aed3079 100644
    --- a/core/src/main/java/hudson/util/KeyedDataStorage.java
    +++ b/core/src/main/java/hudson/util/KeyedDataStorage.java
    @@ -254,7 +254,7 @@ public abstract class KeyedDataStorage<T,P> {
          * Among cache misses, number of times when we had {@link SoftReference}
          * but lost its value due to GC.
          *
    -     * <tt>totalQuery-cacheHit-weakRefLost</tt> means cache miss.
    +     * {@code totalQuery-cacheHit-weakRefLost} means cache miss.
          */
         public final AtomicInteger weakRefLost = new AtomicInteger();
         /**
    diff --git a/core/src/main/java/hudson/util/LineEndNormalizingWriter.java b/core/src/main/java/hudson/util/LineEndNormalizingWriter.java
    index 2a0edee1d031c5124e8cb7f6d3b416f59b45137a..480f71d45973dd8519c6d27536ef47a90fe1a88e 100644
    --- a/core/src/main/java/hudson/util/LineEndNormalizingWriter.java
    +++ b/core/src/main/java/hudson/util/LineEndNormalizingWriter.java
    @@ -31,7 +31,7 @@ import java.io.IOException;
      * Finds the lone LF and converts that to CR+LF.
      *
      * <p>
    - * Internet Explorer's <tt>XmlHttpRequest.responseText</tt> seems to
    + * Internet Explorer's {@code XmlHttpRequest.responseText} seems to
      * normalize the line end, and if we only send LF without CR, it will
      * not recognize that as a new line. To work around this problem,
      * we use this filter to always convert LF to CR+LF.
    diff --git a/core/src/main/java/hudson/util/ListBoxModel.java b/core/src/main/java/hudson/util/ListBoxModel.java
    index 63d7ee7795be2c8e80c99c816b6a2c638d413ef2..fc717cc7ef606d98b6dae9cfacbf7ababd5e0af4 100644
    --- a/core/src/main/java/hudson/util/ListBoxModel.java
    +++ b/core/src/main/java/hudson/util/ListBoxModel.java
    @@ -62,7 +62,7 @@ import java.util.Collection;
      *
      * <p>
      * Other parts of the HTML can initiate the SELECT element update by using the "updateListBox"
    - * function, defined in <tt>hudson-behavior.js</tt>. The following example does it
    + * function, defined in {@code hudson-behavior.js}. The following example does it
      * when the value of the textbox changes:
      *
      * <pre>{@code <xmp>
    @@ -70,11 +70,11 @@ import java.util.Collection;
      * }
    * *

    - * The first argument is the SELECT element or the ID of it (see Prototype.js $(...) function.) + * The first argument is the SELECT element or the ID of it (see Prototype.js {@code $(...)} function.) * The second argument is the URL that returns the options list. * *

    - * The URL usually maps to the doXXX method on the server, which uses {@link ListBoxModel} + * The URL usually maps to the {@code doXXX} method on the server, which uses {@link ListBoxModel} * for producing option values. See the following example: * *

    diff --git a/core/src/main/java/hudson/util/LogTaskListener.java b/core/src/main/java/hudson/util/LogTaskListener.java
    index 620e709c71fa11cf1201a18ad735ce5f83580ae3..bb3afc41e1593353228a33a2aca89ac93e7881aa 100644
    --- a/core/src/main/java/hudson/util/LogTaskListener.java
    +++ b/core/src/main/java/hudson/util/LogTaskListener.java
    @@ -27,50 +27,41 @@ package hudson.util;
     import hudson.console.ConsoleNote;
     import hudson.model.TaskListener;
     import java.io.ByteArrayOutputStream;
    +import java.io.Closeable;
     import java.io.IOException;
     import java.io.OutputStream;
     import java.io.PrintStream;
    -import java.io.PrintWriter;
    -import java.io.Serializable;
     import java.util.logging.Level;
     import java.util.logging.LogRecord;
     import java.util.logging.Logger;
     
    +// TODO: AbstractTaskListener is empty now, but there are dependencies on that e.g. Ruby Runtime - JENKINS-48116)
    +// The change needs API deprecation policy or external usages cleanup.
    +
     /**
      * {@link TaskListener} which sends messages to a {@link Logger}.
      */
    -public class LogTaskListener extends AbstractTaskListener implements Serializable {
    -    
    +public class LogTaskListener extends AbstractTaskListener implements TaskListener, Closeable {
    +
    +    // would be simpler to delegate to the LogOutputStream but this would incompatibly change the serial form
         private final TaskListener delegate;
     
         public LogTaskListener(Logger logger, Level level) {
             delegate = new StreamTaskListener(new LogOutputStream(logger, level, new Throwable().getStackTrace()[1]));
         }
     
    +    @Override
         public PrintStream getLogger() {
             return delegate.getLogger();
         }
     
    -    public PrintWriter error(String msg) {
    -        return delegate.error(msg);
    -    }
    -
    -    public PrintWriter error(String format, Object... args) {
    -        return delegate.error(format, args);
    -    }
    -
    -    public PrintWriter fatalError(String msg) {
    -        return delegate.fatalError(msg);
    -    }
    -
    -    public PrintWriter fatalError(String format, Object... args) {
    -        return delegate.fatalError(format, args);
    -    }
    -
    +    @Override
    +    @SuppressWarnings("rawtypes")
         public void annotate(ConsoleNote ann) {
             // no annotation support
         }
     
    +    @Override
         public void close() {
             delegate.getLogger().close();
         }
    @@ -82,12 +73,13 @@ public class LogTaskListener extends AbstractTaskListener implements Serializabl
             private final StackTraceElement caller;
             private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
     
    -        public LogOutputStream(Logger logger, Level level, StackTraceElement caller) {
    +        LogOutputStream(Logger logger, Level level, StackTraceElement caller) {
                 this.logger = logger;
                 this.level = level;
                 this.caller = caller;
             }
     
    +        @Override
             public void write(int b) throws IOException {
                 if (b == '\r' || b == '\n') {
                     flush();
    diff --git a/core/src/main/java/hudson/util/Memoizer.java b/core/src/main/java/hudson/util/Memoizer.java
    index a69d60810455097783f269cb3bdf39b5f8710a20..8e4057100b10c0c4705c2136e30322d850110dd8 100644
    --- a/core/src/main/java/hudson/util/Memoizer.java
    +++ b/core/src/main/java/hudson/util/Memoizer.java
    @@ -34,7 +34,9 @@ import java.util.concurrent.ConcurrentHashMap;
      *
      * @author Kohsuke Kawaguchi
      * @since 1.281
    + * @deprecated Simply use {@link ConcurrentHashMap#computeIfAbsent}.
      */
    +@Deprecated
     public abstract class Memoizer {
         private final ConcurrentHashMap store = new ConcurrentHashMap();
     
    diff --git a/core/src/main/java/hudson/util/NoHomeDir.java b/core/src/main/java/hudson/util/NoHomeDir.java
    index cae6089554a16dd95e2af0d1399862daf15422e3..c3d15ea367762b52a65592fa721f5bb176a5cf38 100644
    --- a/core/src/main/java/hudson/util/NoHomeDir.java
    +++ b/core/src/main/java/hudson/util/NoHomeDir.java
    @@ -30,7 +30,7 @@ import java.io.File;
      * we couldn't create the home directory.
      *
      * 

    - * index.jelly would display a nice friendly error page. + * {@code index.jelly} would display a nice friendly error page. * * @author Kohsuke Kawaguchi */ diff --git a/core/src/main/java/hudson/util/NoTempDir.java b/core/src/main/java/hudson/util/NoTempDir.java index 10cfceb5ac04e446d2bb0c45c1836a8b2130a341..2e6209463dc814ed173c25e477e2665aab46e5b8 100644 --- a/core/src/main/java/hudson/util/NoTempDir.java +++ b/core/src/main/java/hudson/util/NoTempDir.java @@ -33,7 +33,7 @@ import java.io.IOException; * there appears to be no temporary directory. * *

    - * index.jelly would display a nice friendly error page. + * {@code index.jelly} would display a nice friendly error page. * * @author Kohsuke Kawaguchi */ diff --git a/core/src/main/java/hudson/util/PersistedList.java b/core/src/main/java/hudson/util/PersistedList.java index 17fcc663812c14c92cadf39f40a422a944db2608..ae27fbfc481e27bd157265553d7fc527c4893ed1 100644 --- a/core/src/main/java/hudson/util/PersistedList.java +++ b/core/src/main/java/hudson/util/PersistedList.java @@ -45,7 +45,7 @@ import java.util.List; * Collection whose change is notified to the parent object for persistence. * * @author Kohsuke Kawaguchi - * @since 1.MULTISOURCE + * @since 1.333 */ public class PersistedList extends AbstractList { protected final CopyOnWriteList data = new CopyOnWriteList(); diff --git a/core/src/main/java/hudson/util/PluginServletFilter.java b/core/src/main/java/hudson/util/PluginServletFilter.java index 9b311aa790b1ad73c40f304869ce59cf3ee0e573..08ab06151193d420d91da07871fb951671301e41 100644 --- a/core/src/main/java/hudson/util/PluginServletFilter.java +++ b/core/src/main/java/hudson/util/PluginServletFilter.java @@ -113,6 +113,25 @@ public class PluginServletFilter implements Filter, ExtensionPoint { } } + /** + * Checks whether the given filter is already registered in the chain. + * @param filter the filter to check. + * @return true if the filter is already registered in the chain. + * @since 2.94 + */ + public static boolean hasFilter(Filter filter) { + Jenkins j = Jenkins.getInstanceOrNull(); + PluginServletFilter container = null; + if(j != null) { + container = getInstance(j.servletContext); + } + if (j == null || container == null) { + return LEGACY.contains(filter); + } else { + return container.list.contains(filter); + } + } + public static void removeFilter(Filter filter) throws ServletException { Jenkins j = Jenkins.getInstanceOrNull(); if (j==null || getInstance(j.servletContext) == null) { @@ -147,7 +166,11 @@ public class PluginServletFilter implements Filter, ExtensionPoint { @Restricted(NoExternalUse.class) public static void cleanUp() { - PluginServletFilter instance = getInstance(Jenkins.getInstance().servletContext); + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins == null) { + return; + } + PluginServletFilter instance = getInstance(jenkins.servletContext); if (instance != null) { // While we could rely on the current implementation of list being a CopyOnWriteArrayList // safer to just take an explicit copy of the list and operate on the copy diff --git a/core/src/main/java/hudson/util/ProcessTree.java b/core/src/main/java/hudson/util/ProcessTree.java index 95fe237fc32253811d9d308dab3da0aab181218a..1e40afdb0988ed5f64db1adfebc0e3cda3e2530a 100644 --- a/core/src/main/java/hudson/util/ProcessTree.java +++ b/core/src/main/java/hudson/util/ProcessTree.java @@ -40,10 +40,20 @@ import hudson.util.ProcessTree.OSProcess; import hudson.util.ProcessTreeRemoting.IOSProcess; import hudson.util.ProcessTreeRemoting.IProcessTree; import jenkins.security.SlaveToMasterCallable; +import jenkins.util.java.JavaUtils; import org.jvnet.winp.WinProcess; import org.jvnet.winp.WinpException; -import java.io.*; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -56,6 +66,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.SortedMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -64,7 +75,6 @@ import javax.annotation.CheckForNull; import static com.sun.jna.Pointer.NULL; import jenkins.util.SystemProperties; import static hudson.util.jna.GNUCLibrary.LIBC; -import static java.util.logging.Level.FINE; import static java.util.logging.Level.FINER; import static java.util.logging.Level.FINEST; import javax.annotation.Nonnull; @@ -94,9 +104,20 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * Lazily obtained {@link ProcessKiller}s to be applied on this process tree. */ private transient volatile List killers; + + /** + * Flag to skip the veto check since there aren't any. + */ + private boolean skipVetoes; // instantiation only allowed for subtypes in this class - private ProcessTree() {} + private ProcessTree() { + skipVetoes = false; + } + + private ProcessTree(boolean vetoesExist) { + skipVetoes = !vetoesExist; + } /** * Gets the process given a specific ID, or null if no such process exists. @@ -133,6 +154,8 @@ public abstract class ProcessTree implements Iterable, IProcessTree, */ public abstract void killAll(Map modelEnvVars) throws InterruptedException; + private final long softKillWaitSeconds = Integer.getInteger("SoftKillWaitSeconds", 2 * 60); // by default processes get at most 2 minutes to respond to SIGTERM (JENKINS-17116) + /** * Convenience method that does {@link #killAll(Map)} and {@link OSProcess#killRecursively()}. * This is necessary to reliably kill the process and its descendants, as some OS @@ -156,22 +179,24 @@ public abstract class ProcessTree implements Iterable, IProcessTree, try { VirtualChannel channelToMaster = SlaveComputer.getChannelToMaster(); if (channelToMaster!=null) { - killers = channelToMaster.call(new SlaveToMasterCallable, IOException>() { - public List call() throws IOException { - return new ArrayList(ProcessKiller.all()); - } - }); + killers = channelToMaster.call(new ListAll()); } else { // used in an environment that doesn't support talk-back to the master. // let's do with what we have. killers = Collections.emptyList(); } - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to obtain killers",e); + } catch (IOException | Error e) { + LOGGER.log(Level.WARNING, "Failed to obtain killers", e); killers = Collections.emptyList(); } return killers; } + private static class ListAll extends SlaveToMasterCallable, IOException> { + @Override + public List call() throws IOException { + return new ArrayList<>(ProcessKiller.all()); + } + } /** * Represents a process. @@ -217,10 +242,11 @@ public abstract class ProcessTree implements Iterable, IProcessTree, void killByKiller() throws InterruptedException { for (ProcessKiller killer : getKillers()) try { - if (killer.kill(this)) + if (killer.kill(this)) { break; - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to kill pid="+getPid(),e); + } + } catch (IOException | Error e) { + LOGGER.log(Level.WARNING, "Failed to kill pid=" + getPid(), e); } } @@ -239,14 +265,26 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * null if no one objects killing the process. */ protected @CheckForNull VetoCause getVeto() { - for (ProcessKillingVeto vetoExtension : ProcessKillingVeto.all()) { - VetoCause cause = vetoExtension.vetoProcessKilling(this); - if (cause != null) { - if (LOGGER.isLoggable(FINEST)) - LOGGER.finest("Killing of pid " + getPid() + " vetoed by " + vetoExtension.getClass().getName() + ": " + cause.getMessage()); - return cause; + String causeMessage = null; + + // Quick check, does anything exist to check against + if (!skipVetoes) { + try { + VirtualChannel channelToMaster = SlaveComputer.getChannelToMaster(); + if (channelToMaster!=null) { + CheckVetoes vetoCheck = new CheckVetoes(this); + causeMessage = channelToMaster.call(vetoCheck); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "I/O Exception while checking for vetoes", e); + } catch (InterruptedException e) { + LOGGER.log(Level.WARNING, "Interrupted Exception while checking for vetoes", e); } } + + if (causeMessage != null) { + return new VetoCause(causeMessage); + } return null; } @@ -298,6 +336,27 @@ public abstract class ProcessTree implements Iterable, IProcessTree, Object writeReplace() { return new SerializedProcess(pid); } + + private class CheckVetoes extends SlaveToMasterCallable { + private IOSProcess process; + + public CheckVetoes(IOSProcess processToCheck) { + process = processToCheck; + } + + @Override + public String call() throws IOException { + for (ProcessKillingVeto vetoExtension : ProcessKillingVeto.all()) { + VetoCause cause = vetoExtension.vetoProcessKilling(process); + if (cause != null) { + if (LOGGER.isLoggable(FINEST)) + LOGGER.info("Killing of pid " + getPid() + " vetoed by " + vetoExtension.getClass().getName() + ": " + cause.getMessage()); + return cause.getMessage(); + } + } + return null; + } + } } /** @@ -336,6 +395,8 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } + /* package */ static Boolean vetoersExist; + /** * Gets the {@link ProcessTree} of the current system * that JVM runs in, or in the worst case return the default one @@ -345,17 +406,35 @@ public abstract class ProcessTree implements Iterable, IProcessTree, if(!enabled) return DEFAULT; + // Check for the existance of vetoers if I don't know already + if (vetoersExist == null) { + try { + VirtualChannel channelToMaster = SlaveComputer.getChannelToMaster(); + if (channelToMaster != null) { + vetoersExist = channelToMaster.call(new DoVetoersExist()); + } + } + catch (Exception e) { + LOGGER.log(Level.WARNING, "Error while determining if vetoers exist", e); + } + } + + // Null-check in case the previous call worked + boolean vetoes = (vetoersExist == null ? true : vetoersExist); + try { if(File.pathSeparatorChar==';') - return new Windows(); + return new Windows(vetoes); String os = Util.fixNull(System.getProperty("os.name")); if(os.equals("Linux")) - return new Linux(); + return new Linux(vetoes); + if(os.equals("AIX")) + return new AIX(vetoes); if(os.equals("SunOS")) - return new Solaris(); + return new Solaris(vetoes); if(os.equals("Mac OS X")) - return new Darwin(); + return new Darwin(vetoes); } catch (LinkageError e) { LOGGER.log(Level.WARNING,"Failed to load winp. Reverting to the default",e); enabled = false; @@ -363,6 +442,13 @@ public abstract class ProcessTree implements Iterable, IProcessTree, return DEFAULT; } + + private static class DoVetoersExist extends SlaveToMasterCallable { + @Override + public Boolean call() throws IOException { + return ProcessKillingVeto.all().size() > 0; + } + } // // @@ -430,6 +516,8 @@ public abstract class ProcessTree implements Iterable, IProcessTree, return; LOGGER.log(FINER, "Killing recursively {0}", getPid()); + // Firstly try to kill the root process gracefully, then do a forcekill if it does not help (algorithm is described in JENKINS-17116) + killSoftly(); p.killRecursively(); killByKiller(); } @@ -441,10 +529,39 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } LOGGER.log(FINER, "Killing {0}", getPid()); + // Firstly try to kill it gracefully, then do a forcekill if it does not help (algorithm is described in JENKINS-17116) + killSoftly(); p.kill(); killByKiller(); } + private void killSoftly() throws InterruptedException { + // send Ctrl+C to the process + try { + if (!p.sendCtrlC()) { + return; + } + } + catch (WinpException e) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Failed to send CTRL+C to pid=" + getPid(), e); + } + return; + } + + // after that wait for it to cease to exist + long deadline = System.nanoTime() + softKillWaitSeconds * 1000000000; + int sleepTime = 10; // initially we sleep briefly, then sleep up to 1sec + do { + if (!p.isRunning()) { + break; + } + + Thread.sleep(sleepTime); + sleepTime = Math.min(sleepTime * 2, 1000); + } while (System.nanoTime() < deadline); + } + @Override public synchronized List getArguments() { if(args==null) { @@ -510,7 +627,9 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } private static final class Windows extends Local { - Windows() { + Windows(boolean vetoesExist) { + super(vetoesExist); + for (final WinProcess p : WinProcess.all()) { int pid = p.getPid(); if(pid == 0 || pid == 4) continue; // skip the System Idle and System processes @@ -572,15 +691,13 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } static abstract class Unix extends Local { + public Unix(boolean vetoersExist) { + super(vetoersExist); + } + @Override public OSProcess get(Process proc) { - try { - return get((Integer) UnixReflection.PID_FIELD.get(proc)); - } catch (IllegalAccessException e) { // impossible - IllegalAccessError x = new IllegalAccessError(); - x.initCause(e); - throw x; - } + return get(UnixReflection.pid(proc)); } public void killAll(Map modelEnvVars) throws InterruptedException { @@ -593,7 +710,9 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * {@link ProcessTree} based on /proc. */ static abstract class ProcfsUnix extends Unix { - ProcfsUnix() { + ProcfsUnix(boolean vetoersExist) { + super(vetoersExist); + File[] processes = new File("/proc").listFiles(new FileFilter() { public boolean accept(File f) { return f.isDirectory(); @@ -639,12 +758,29 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * Tries to kill this process. */ public void kill() throws InterruptedException { + // after sending SIGTERM, wait for the process to cease to exist + long deadline = System.nanoTime() + softKillWaitSeconds * 1000000000; + kill(deadline); + } + + private void kill(long deadline) throws InterruptedException { if (getVeto() != null) return; try { int pid = getPid(); LOGGER.fine("Killing pid="+pid); UnixReflection.destroy(pid); + // after sending SIGTERM, wait for the process to cease to exist + int sleepTime = 10; // initially we sleep briefly, then sleep up to 1sec + File status = getFile("status"); + do { + if (!status.exists()) { + break; // status is gone, process therefore as well + } + + Thread.sleep(sleepTime); + sleepTime = Math.min(sleepTime * 2, 1000); + } while (System.nanoTime() < deadline); } catch (IllegalAccessException e) { // this is impossible IllegalAccessError x = new IllegalAccessError(); @@ -661,11 +797,22 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } public void killRecursively() throws InterruptedException { + // after sending SIGTERM, wait for the processes to cease to exist until the deadline + long deadline = System.nanoTime() + softKillWaitSeconds * 1000000000; + killRecursively(deadline); + } + + private void killRecursively(long deadline) throws InterruptedException { // We kill individual processes of a tree, so handling vetoes inside #kill() is enough for UnixProcess es LOGGER.fine("Recursively killing pid="+getPid()); - for (OSProcess p : getChildren()) - p.killRecursively(); - kill(); + for (OSProcess p : getChildren()) { + if (p instanceof UnixProcess) { + ((UnixProcess)p).killRecursively(deadline); + } else { + p.killRecursively(); // should not happen, fallback to non-deadline version + } + } + kill(deadline); } /** @@ -678,65 +825,97 @@ public abstract class ProcessTree implements Iterable, IProcessTree, public abstract List getArguments(); } + //TODO: can be replaced by multi-release JAR /** * Reflection used in the Unix support. */ private static final class UnixReflection { /** * Field to access the PID of the process. + * Required for Java 8 and older JVMs. */ - private static final Field PID_FIELD; + private static final Field JAVA8_PID_FIELD; + + /** + * Field to access the PID of the process. + * Required for Java 9 and above until this is replaced by multi-release JAR. + */ + private static final Method JAVA9_PID_METHOD; /** * Method to destroy a process, given pid. * * Looking at the JavaSE source code, this is using SIGTERM (15) */ - private static final Method DESTROY_PROCESS; + private static final Method JAVA8_DESTROY_PROCESS; + private static final Method JAVA_9_PROCESSHANDLE_OF; + private static final Method JAVA_9_PROCESSHANDLE_DESTROY; static { try { - Class clazz = Class.forName("java.lang.UNIXProcess"); - PID_FIELD = clazz.getDeclaredField("pid"); - PID_FIELD.setAccessible(true); - - if (isPreJava8()) { - DESTROY_PROCESS = clazz.getDeclaredMethod("destroyProcess",int.class); + if (JavaUtils.isRunningWithPostJava8()) { + Class clazz = Process.class; + JAVA9_PID_METHOD = clazz.getMethod("pid"); + JAVA8_PID_FIELD = null; + Class processHandleClazz = Class.forName("java.lang.ProcessHandle"); + JAVA_9_PROCESSHANDLE_OF = processHandleClazz.getMethod("of", long.class); + JAVA_9_PROCESSHANDLE_DESTROY = processHandleClazz.getMethod("destroy"); + JAVA8_DESTROY_PROCESS = null; } else { - DESTROY_PROCESS = clazz.getDeclaredMethod("destroyProcess",int.class, boolean.class); + Class clazz = Class.forName("java.lang.UNIXProcess"); + JAVA8_PID_FIELD = clazz.getDeclaredField("pid"); + JAVA8_PID_FIELD.setAccessible(true); + JAVA9_PID_METHOD = null; + + JAVA8_DESTROY_PROCESS = clazz.getDeclaredMethod("destroyProcess", int.class, boolean.class); + JAVA8_DESTROY_PROCESS.setAccessible(true); + JAVA_9_PROCESSHANDLE_OF = null; + JAVA_9_PROCESSHANDLE_DESTROY = null; } - DESTROY_PROCESS.setAccessible(true); - } catch (ClassNotFoundException e) { - LinkageError x = new LinkageError(); - x.initCause(e); - throw x; - } catch (NoSuchFieldException e) { - LinkageError x = new LinkageError(); - x.initCause(e); - throw x; - } catch (NoSuchMethodException e) { - LinkageError x = new LinkageError(); - x.initCause(e); + } catch (ClassNotFoundException | NoSuchFieldException | NoSuchMethodException e) { + LinkageError x = new LinkageError("Cannot initialize reflection for Unix Processes", e); throw x; } } - public static void destroy(int pid) throws IllegalAccessException, InvocationTargetException { - if (isPreJava8()) { - DESTROY_PROCESS.invoke(null, pid); + public static void destroy(int pid) throws IllegalAccessException, + InvocationTargetException { + if (JAVA8_DESTROY_PROCESS != null) { + JAVA8_DESTROY_PROCESS.invoke(null, pid, false); } else { - DESTROY_PROCESS.invoke(null, pid, false); + final Optional handle = (Optional)JAVA_9_PROCESSHANDLE_OF.invoke(null, pid); + if (handle.isPresent()) { + JAVA_9_PROCESSHANDLE_DESTROY.invoke(handle.get()); + } } } - private static boolean isPreJava8() { - int javaVersionAsAnInteger = Integer.parseInt(System.getProperty("java.version").replaceAll("\\.", "").replaceAll("_", "").substring(0, 2)); - return javaVersionAsAnInteger < 18; + //TODO: We ideally need to update ProcessTree APIs to Support Long (JENKINS-53799). + public static int pid(@Nonnull Process proc) { + try { + if (JAVA8_PID_FIELD != null) { + return JAVA8_PID_FIELD.getInt(proc); + } else { + long pid = (long)JAVA9_PID_METHOD.invoke(proc); + if (pid > Integer.MAX_VALUE) { + throw new IllegalAccessError("Java 9+ support error (JENKINS-53799). PID is out of Jenkins API bounds: " + pid); + } + return (int)pid; + } + } catch (IllegalAccessException | InvocationTargetException e) { // impossible + IllegalAccessError x = new IllegalAccessError(); + x.initCause(e); + throw x; + } } } static class Linux extends ProcfsUnix { + public Linux(boolean vetoersExist) { + super(vetoersExist); + } + protected LinuxProcess createProcess(int pid) throws IOException { return new LinuxProcess(pid); } @@ -825,7 +1004,328 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } /** - * Implementation for Solaris that uses /proc. + * Implementation for AIX that uses {@code /proc}. + * + * /proc/PID/status contains a pstatus struct. We use it to determine if the process is 32 or 64 bit + * + * /proc/PID/psinfo contains a psinfo struct. We use it to determine where the + * process arguments and environment are located in PID's address space. + * + * /proc/PID/as contains the address space of the process we are inspecting. We can + * follow the pr_envp and pr_argv pointers from psinfo to find the vectors to the + * environment variables and process arguments, respectvely. When following pointers + * in this address space we need to make sure to use 32-bit or 64-bit pointers + * depending on what sized pointers PID uses, regardless of what size pointers + * the Java process uses. + * + * Note that the size of a 64-bit address space is larger than Long.MAX_VALUE (because + * longs are signed). So normal Java utilities like RandomAccessFile and FileChannel + * (which use signed longs as offsets) are not able to read from the end of the address + * space, where envp and argv will be. Therefore we need to use LIBC.pread() directly. + * when accessing this file. + */ + static class AIX extends ProcfsUnix { + public AIX(boolean vetoersExist) { + super(vetoersExist); + } + + protected OSProcess createProcess(final int pid) throws IOException { + return new AIXProcess(pid); + } + + private class AIXProcess extends UnixProcess { + private static final byte PR_MODEL_ILP32 = 0; + private static final byte PR_MODEL_LP64 = 1; + + /* + * An arbitrary upper-limit on how many characters readLine() will + * try reading before giving up. This avoids having readLine() loop + * over the entire process address space if this class has bugs. + */ + private final int LINE_LENGTH_LIMIT = + SystemProperties.getInteger(AIX.class.getName()+".lineLimit", 10000); + + /* + * True if target process is 64-bit (Java process may be different). + */ + private final boolean b64; + + private final int ppid; + + private final long pr_envp; + private final long pr_argp; + private final int argc; + private EnvVars envVars; + private List arguments; + + private AIXProcess(int pid) throws IOException { + super(pid); + + RandomAccessFile pstatus = new RandomAccessFile(getFile("status"),"r"); + try { + // typedef struct pstatus { + // uint32_t pr_flag; /* process flags from proc struct p_flag */ + // uint32_t pr_flag2; /* process flags from proc struct p_flag2 */ + // uint32_t pr_flags; /* /proc flags */ + // uint32_t pr_nlwp; /* number of threads in the process */ + // char pr_stat; /* process state from proc p_stat */ + // char pr_dmodel; /* data model for the process */ + // char pr__pad1[6]; /* reserved for future use */ + // pr_sigset_t pr_sigpend; /* set of process pending signals */ + // prptr64_t pr_brkbase; /* address of the process heap */ + // uint64_t pr_brksize; /* size of the process heap, in bytes */ + // prptr64_t pr_stkbase; /* address of the process stack */ + // uint64_t pr_stksize; /* size of the process stack, in bytes */ + // uint64_t pr_pid; /* process id */ + // uint64_t pr_ppid; /* parent process id */ + // uint64_t pr_pgid; /* process group id */ + // uint64_t pr_sid; /* session id */ + // pr_timestruc64_t pr_utime; /* process user cpu time */ + // pr_timestruc64_t pr_stime; /* process system cpu time */ + // pr_timestruc64_t pr_cutime; /* sum of children's user times */ + // pr_timestruc64_t pr_cstime; /* sum of children's system times */ + // pr_sigset_t pr_sigtrace; /* mask of traced signals */ + // fltset_t pr_flttrace; /* mask of traced hardware faults */ + // uint32_t pr_sysentry_offset; /* offset into pstatus file of sysset_t + // * identifying system calls traced on + // * entry. If 0, then no entry syscalls + // * are being traced. */ + // uint32_t pr_sysexit_offset; /* offset into pstatus file of sysset_t + // * identifying system calls traced on + // * exit. If 0, then no exit syscalls + // * are being traced. */ + // uint64_t pr__pad[8]; /* reserved for future use */ + // lwpstatus_t pr_lwp; /* "representative" thread status */ + // } pstatus_t; + + pstatus.seek(17); // offset of pr_dmodel + + byte pr_dmodel = pstatus.readByte(); + + if (pr_dmodel == PR_MODEL_ILP32) { + b64 = false; + } else if (pr_dmodel == PR_MODEL_LP64) { + b64 = true; + } else { + throw new IOException("Unrecognized data model value"); // sanity check + } + + pstatus.seek(88); // offset of pr_pid + + if (adjust((int)pstatus.readLong()) != pid) + throw new IOException("pstatus PID mismatch"); // sanity check + + ppid = adjust((int)pstatus.readLong()); // AIX pids are stored as a 64 bit integer, + // but the first 4 bytes are always 0 + + } finally { + pstatus.close(); + } + + RandomAccessFile psinfo = new RandomAccessFile(getFile("psinfo"),"r"); + try { + // typedef struct psinfo { + // uint32_t pr_flag; /* process flags from proc struct p_flag */ + // uint32_t pr_flag2; /* process flags from proc struct p_flag2 * + // uint32_t pr_nlwp; /* number of threads in process */ + // uint32_t pr__pad1; /* reserved for future use */ + // uint64_t pr_uid; /* real user id */ + // uint64_t pr_euid; /* effective user id */ + // uint64_t pr_gid; /* real group id */ + // uint64_t pr_egid; /* effective group id */ + // uint64_t pr_pid; /* unique process id */ + // uint64_t pr_ppid; /* process id of parent */ + // uint64_t pr_pgid; /* pid of process group leader */ + // uint64_t pr_sid; /* session id */ + // uint64_t pr_ttydev; /* controlling tty device */ + // prptr64_t pr_addr; /* internal address of proc struct */ + // uint64_t pr_size; /* process image size in kb (1024) units */ + // uint64_t pr_rssize; /* resident set size in kb (1024) units */ + // pr_timestruc64_t pr_start; /* process start time, time since epoch */ + // pr_timestruc64_t pr_time; /* usr+sys cpu time for this process */ + // cid_t pr_cid; /* corral id */ + // ushort_t pr__pad2; /* reserved for future use */ + // uint32_t pr_argc; /* initial argument count */ + // prptr64_t pr_argv; /* address of initial argument vector in + // * user process */ + // prptr64_t pr_envp; /* address of initial environment vector + // * in user process */ + // char pr_fname[prfnsz]; /* last component of exec()ed pathname*/ + // char pr_psargs[prargsz]; /* initial characters of arg list */ + // uint64_t pr__pad[8]; /* reserved for future use */ + // struct lwpsinfo pr_lwp; /* "representative" thread info */ + // } + + psinfo.seek(48); // offset of pr_pid + + if (adjust((int)psinfo.readLong()) != pid) + throw new IOException("psinfo PID mismatch"); // sanity check + + if (adjust((int)psinfo.readLong()) != ppid) + throw new IOException("psinfo PPID mismatch"); // sanity check + + psinfo.seek(148); // offset of pr_argc + + argc = adjust(psinfo.readInt()); + pr_argp = adjustL(psinfo.readLong()); + pr_envp = adjustL(psinfo.readLong()); + } finally { + psinfo.close(); + } + } + + public OSProcess getParent() { + return get(ppid); + } + + public synchronized List getArguments() { + if (arguments != null) + return arguments; + + arguments = new ArrayList(argc); + if (argc == 0) { + return arguments; + } + + try { + int psize = b64 ? 8 : 4; + Memory m = new Memory(psize); + int fd = LIBC.open(getFile("as").getAbsolutePath(), 0); + + try { + // Get address of the argument vector + LIBC.pread(fd, m, new NativeLong(psize), new NativeLong(pr_argp)); + long argp = b64 ? m.getLong(0) : to64(m.getInt(0)); + + if (argp == 0) // Should never happen + return arguments; + + // Itterate through argument vector + for( int n=0; ; n++ ) { + + LIBC.pread(fd, m, new NativeLong(psize), new NativeLong(argp+(n*psize))); + long addr = b64 ? m.getLong(0) : to64(m.getInt(0)); + + if (addr == 0) // completed the walk + break; + + // now read the null-terminated string + arguments.add(readLine(fd, addr, "arg["+ n +"]")); + } + } finally { + LIBC.close(fd); + } + } catch (IOException | LastErrorException e) { + // failed to read. this can happen under normal circumstances (most notably permission denied) + // so don't report this as an error. + } + + arguments = Collections.unmodifiableList(arguments); + return arguments; + } + + public synchronized EnvVars getEnvironmentVariables() { + if(envVars != null) + return envVars; + envVars = new EnvVars(); + + if (pr_envp == 0) { + return envVars; + } + + try { + int psize = b64 ? 8 : 4; + Memory m = new Memory(psize); + int fd = LIBC.open(getFile("as").getAbsolutePath(), 0); + + try { + // Get address of the environment vector + LIBC.pread(fd, m, new NativeLong(psize), new NativeLong(pr_envp)); + long envp = b64 ? m.getLong(0) : to64(m.getInt(0)); + + if (envp == 0) // Should never happen + return envVars; + + // Itterate through environment vector + for( int n=0; ; n++ ) { + + LIBC.pread(fd, m, new NativeLong(psize), new NativeLong(envp+(n*psize))); + long addr = b64 ? m.getLong(0) : to64(m.getInt(0)); + + if (addr == 0) // completed the walk + break; + + // now read the null-terminated string + envVars.addLine(readLine(fd, addr, "env["+ n +"]")); + } + } finally { + LIBC.close(fd); + } + } catch (IOException | LastErrorException e) { + // failed to read. this can happen under normal circumstances (most notably permission denied) + // so don't report this as an error. + } + return envVars; + } + + private String readLine(int fd, long addr, String prefix) throws IOException { + if(LOGGER.isLoggable(FINEST)) + LOGGER.finest("Reading "+prefix+" at "+addr); + + Memory m = new Memory(1); + byte ch = 1; + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int i = 0; + while(true) { + if (i++ > LINE_LENGTH_LIMIT) { + LOGGER.finest("could not find end of line, giving up"); + throw new IOException("could not find end of line, giving up"); + } + + long r = LIBC.pread(fd, m, new NativeLong(1), new NativeLong(addr)); + ch = m.getByte(0); + + if (ch == 0) + break; + buf.write(ch); + addr++; + } + String line = buf.toString(); + if(LOGGER.isLoggable(FINEST)) + LOGGER.finest(prefix+" was "+line); + return line; + } + } + + /** + * int to long conversion with zero-padding. + */ + private static long to64(int i) { + return i&0xFFFFFFFFL; + } + + /** + * {@link DataInputStream} reads a value in big-endian, so + * convert it to the correct value on little-endian systems. + */ + private static int adjust(int i) { + if(IS_LITTLE_ENDIAN) + return (i<<24) |((i<<8) & 0x00FF0000) | ((i>>8) & 0x0000FF00) | (i>>>24); + else + return i; + } + + public static long adjustL(long i) { + if(IS_LITTLE_ENDIAN) { + return Long.reverseBytes(i); + } else { + return i; + } + } + } + + /** + * Implementation for Solaris that uses {@code /proc}. * * /proc/PID/psinfo contains a psinfo_t struct. We use it to determine where the * process arguments and environment are located in PID's address space. @@ -851,6 +1351,10 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * when accessing this file. */ static class Solaris extends ProcfsUnix { + public Solaris(boolean vetoersExist) { + super(vetoersExist); + } + protected OSProcess createProcess(final int pid) throws IOException { return new SolarisProcess(pid); } @@ -1091,7 +1595,9 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * Implementation for Mac OS X based on sysctl(3). */ private static class Darwin extends Unix { - Darwin() { + Darwin(boolean vetoersExist) { + super(vetoersExist); + String arch = System.getProperty("sun.arch.data.model"); if ("64".equals(arch)) { sizeOf_kinfo_proc = sizeOf_kinfo_proc_64; @@ -1103,18 +1609,18 @@ public abstract class ProcessTree implements Iterable, IProcessTree, kinfo_proc_ppid_offset = kinfo_proc_ppid_offset_32; } try { - IntByReference _ = new IntByReference(sizeOfInt); + IntByReference ref = new IntByReference(sizeOfInt); IntByReference size = new IntByReference(sizeOfInt); Memory m; int nRetry = 0; while(true) { // find out how much memory we need to do this - if(LIBC.sysctl(MIB_PROC_ALL,3, NULL, size, NULL, _)!=0) + if(LIBC.sysctl(MIB_PROC_ALL,3, NULL, size, NULL, ref)!=0) throw new IOException("Failed to obtain memory requirement: "+LIBC.strerror(Native.getLastError())); // now try the real call m = new Memory(size.getValue()); - if(LIBC.sysctl(MIB_PROC_ALL,3, m, size, NULL, _)!=0) { + if(LIBC.sysctl(MIB_PROC_ALL,3, m, size, NULL, ref)!=0) { if(Native.getLastError()==ENOMEM && nRetry++<16) continue; // retry throw new IOException("Failed to call kern.proc.all: "+LIBC.strerror(Native.getLastError())); @@ -1174,14 +1680,14 @@ public abstract class ProcessTree implements Iterable, IProcessTree, arguments = new ArrayList(); envVars = new EnvVars(); - IntByReference _ = new IntByReference(); + IntByReference intByRef = new IntByReference(); IntByReference argmaxRef = new IntByReference(0); IntByReference size = new IntByReference(sizeOfInt); // for some reason, I was never able to get sysctlbyname work. // if(LIBC.sysctlbyname("kern.argmax", argmaxRef.getPointer(), size, NULL, _)!=0) - if(LIBC.sysctl(new int[]{CTL_KERN,KERN_ARGMAX},2, argmaxRef.getPointer(), size, NULL, _)!=0) + if(LIBC.sysctl(new int[]{CTL_KERN,KERN_ARGMAX},2, argmaxRef.getPointer(), size, NULL, intByRef)!=0) throw new IOException("Failed to get kern.argmax: "+LIBC.strerror(Native.getLastError())); int argmax = argmaxRef.getValue(); @@ -1219,7 +1725,7 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } StringArrayMemory m = new StringArrayMemory(argmax); size.setValue(argmax); - if(LIBC.sysctl(new int[]{CTL_KERN,KERN_PROCARGS2,pid},3, m, size, NULL, _)!=0) + if(LIBC.sysctl(new int[]{CTL_KERN,KERN_PROCARGS2,pid},3, m, size, NULL, intByRef)!=0) throw new IOException("Failed to obtain ken.procargs2: "+LIBC.strerror(Native.getLastError())); @@ -1310,8 +1816,13 @@ public abstract class ProcessTree implements Iterable, IProcessTree, * (The opposite of {@link Remote}.) */ public static abstract class Local extends ProcessTree { + @Deprecated Local() { } + + Local(boolean vetoesExist) { + super(vetoesExist); + } } /** @@ -1320,11 +1831,20 @@ public abstract class ProcessTree implements Iterable, IProcessTree, public static class Remote extends ProcessTree implements Serializable { private final IProcessTree proxy; + @Deprecated public Remote(ProcessTree proxy, Channel ch) { this.proxy = ch.export(IProcessTree.class,proxy); for (Entry e : proxy.processes.entrySet()) processes.put(e.getKey(),new RemoteProcess(e.getValue(),ch)); } + + public Remote(ProcessTree proxy, Channel ch, boolean vetoersExist) { + super(vetoersExist); + + this.proxy = ch.export(IProcessTree.class,proxy); + for (Entry e : proxy.processes.entrySet()) + processes.put(e.getKey(),new RemoteProcess(e.getValue(),ch)); + } @Override public OSProcess get(Process proc) { diff --git a/core/src/main/java/hudson/util/ReflectionUtils.java b/core/src/main/java/hudson/util/ReflectionUtils.java index f491b59d8758bffb2f0aba72a306d42bb3283b92..faa6afb99a32ef185dde660d2e8b3bdd1bf67db1 100644 --- a/core/src/main/java/hudson/util/ReflectionUtils.java +++ b/core/src/main/java/hudson/util/ReflectionUtils.java @@ -37,6 +37,7 @@ import java.util.AbstractList; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.CheckForNull; /** * Utility code for reflection. @@ -205,15 +206,23 @@ public class ReflectionUtils extends org.springframework.util.ReflectionUtils { /** * Given the primitive type, returns the VM default value for that type in a boxed form. + * @return null unless {@link Class#isPrimitive} */ - public static Object getVmDefaultValueForPrimitiveType(Class type) { + public static @CheckForNull Object getVmDefaultValueForPrimitiveType(Class type) { return defaultPrimitiveValue.get(type); } - private static final Map defaultPrimitiveValue = new HashMap(); + // TODO the version in org.kohsuke.stapler is incomplete + private static final Map, Object> defaultPrimitiveValue = new HashMap<>(); static { - defaultPrimitiveValue.put(boolean.class,false); - defaultPrimitiveValue.put(int.class,0); - defaultPrimitiveValue.put(long.class,0L); + defaultPrimitiveValue.put(boolean.class, false); + defaultPrimitiveValue.put(char.class, '\0'); + defaultPrimitiveValue.put(byte.class, (byte) 0); + defaultPrimitiveValue.put(short.class, (short) 0); + defaultPrimitiveValue.put(int.class, 0); + defaultPrimitiveValue.put(long.class, 0L); + defaultPrimitiveValue.put(float.class, (float) 0); + defaultPrimitiveValue.put(double.class, (double) 0); + defaultPrimitiveValue.put(void.class, null); // FWIW } } diff --git a/core/src/main/java/hudson/util/RemotingDiagnostics.java b/core/src/main/java/hudson/util/RemotingDiagnostics.java index d38fa7170e9b6629263850f0722b6afcedcf9420..447b19a5c99e486617b8305099498ed531e63e48 100644 --- a/core/src/main/java/hudson/util/RemotingDiagnostics.java +++ b/core/src/main/java/hudson/util/RemotingDiagnostics.java @@ -153,7 +153,10 @@ public final class RemotingDiagnostics { * Obtains the heap dump in an HPROF file. */ public static FilePath getHeapDump(VirtualChannel channel) throws IOException, InterruptedException { - return channel.call(new MasterToSlaveCallable() { + return channel.call(new GetHeapDump()); + } + private static class GetHeapDump extends MasterToSlaveCallable { + @Override public FilePath call() throws IOException { final File hprof = File.createTempFile("hudson-heapdump", "hprof"); hprof.delete(); @@ -169,7 +172,6 @@ public final class RemotingDiagnostics { } private static final long serialVersionUID = 1L; - }); } /** diff --git a/core/src/main/java/hudson/util/Retrier.java b/core/src/main/java/hudson/util/Retrier.java new file mode 100644 index 0000000000000000000000000000000000000000..1e3df9e8b59d373f0a274d233ce65cfc8c541435 --- /dev/null +++ b/core/src/main/java/hudson/util/Retrier.java @@ -0,0 +1,183 @@ +package hudson.util; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.concurrent.Callable; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +/** + * This class implements a process of doing some action repeatedly synchronously until it is performed successfully. + * You can set the number of attempts, the action to perform, the milliseconds to wait for, the definition of success, + * the exceptions that are considered as a failed action, but not an unexpected exception in the action and also the + * listener to manage the expected exceptions happened, just in case it is helpful. + * @param The return type of the action to perform. + */ + +// Limit the use of this class until it is mature enough +@Restricted(NoExternalUse.class) +public class Retrier { + private static final Logger LOGGER = Logger.getLogger(Retrier.class.getName()); + + private int attempts; + private long delay; + private Callable callable; + private BiPredicate checkResult; + private String action; + private BiFunction duringActionExceptionListener; + private Class[] duringActionExceptions; + + private Retrier(Builder builder){ + this.attempts = builder.attempts; + this.delay = builder.delay; + this.callable = builder.callable; + this.checkResult = builder.checkResult; + this.action = builder.action; + this.duringActionExceptionListener = builder.duringActionExceptionListener; + this.duringActionExceptions = builder.duringActionExceptions; + } + + /** + * Start to do retries to perform the set action. + * @return the result of the action, it could be null if there was an exception or if the action itself returns null + * @throws Exception If a unallowed exception is raised during the action + */ + public @CheckForNull V start() throws Exception { + V result = null; + int currentAttempt = 0; + boolean success = false; + + while (currentAttempt < attempts && !success) { + currentAttempt++; + try { + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.log(Level.INFO, Messages.Retrier_Attempt(currentAttempt, action)); + } + result = callable.call(); + + } catch (Exception e) { + if(duringActionExceptions == null || Stream.of(duringActionExceptions).noneMatch(exception -> exception.isAssignableFrom(e.getClass()))) { + // if the raised exception is not considered as a controlled exception doing the action, rethrow it + LOGGER.log(Level.WARNING, Messages.Retrier_ExceptionThrown(currentAttempt, action), e); + throw e; + } else { + // if the exception is considered as a failed action, notify it to the listener + LOGGER.log(Level.INFO, Messages.Retrier_ExceptionFailed(currentAttempt, action), e); + if (duringActionExceptionListener != null) { + LOGGER.log(Level.INFO, Messages.Retrier_CallingListener(e.getLocalizedMessage(), currentAttempt, action)); + result = duringActionExceptionListener.apply(currentAttempt, e); + } + } + } + + // After the call and the call to the listener, which can change the result, test the result + success = checkResult.test(currentAttempt, result); + if (!success) { + if (currentAttempt < attempts) { + LOGGER.log(Level.WARNING, Messages.Retrier_AttemptFailed(currentAttempt, action)); + LOGGER.log(Level.FINE, Messages.Retrier_Sleeping(delay, action)); + try { + Thread.sleep(delay); + } catch (InterruptedException ie) { + LOGGER.log(Level.FINE, Messages.Retrier_Interruption(action)); + Thread.currentThread().interrupt(); // flag this thread as interrupted + currentAttempt = attempts; // finish + } + } else { + // Failed to perform the action + LOGGER.log(Level.INFO, Messages.Retrier_NoSuccess(action, attempts)); + } + } else { + LOGGER.log(Level.INFO, Messages.Retrier_Success(action, currentAttempt)); + } + } + + return result; + } + + /** + * Builder to create a Retrier object. The action to perform, the way of check whether is was + * successful and the name of the action are required. + * @param The return type of the action to perform. + */ + public static class Builder { + private Callable callable; + private String action; + private BiPredicate checkResult; + + private int attempts = 3; + private long delay = 1000; + private BiFunction duringActionExceptionListener; + private Class[] duringActionExceptions; + + /** + * Set the number of attempts trying to perform the action. + * @param attempts number of attempts + * @return this builder + */ + public @Nonnull Builder withAttempts(int attempts) { + this.attempts = attempts; + return this; + } + + /** + * Set the time in milliseconds to wait for the next attempt. + * @param millis milliseconds to wait + * @return this builder + */ + public @Nonnull Builder withDelay(long millis) { + this.delay = millis; + return this; + } + + /** + * Set all the exceptions that are allowed and indicate that the action was failed. When an exception of this + * type or a child type is raised, a listener can be called ({@link #withDuringActionExceptionListener(BiFunction)}). + * In any case, the retrier continues its process, retrying to perform the action again, as it was a normal failure. + * @param exceptions exceptions that indicate that the action was failed. + * @return this builder + */ + public @Nonnull Builder withDuringActionExceptions(@CheckForNull Class[] exceptions) { + this.duringActionExceptions = exceptions; + return this; + } + + /** + * Set the listener to be executed when an allowed exception is raised when performing the action. The listener + * could even change the result of the action if needed. + * @param exceptionListener the listener to call to + * @return this builder + */ + public @Nonnull Builder withDuringActionExceptionListener(@Nonnull BiFunction exceptionListener) { + this.duringActionExceptionListener = exceptionListener; + return this; + } + + /** + * Constructor of the builder with the required parameters. + * @param callable Action to perform + * @param checkResult Method to check if the result of the action was a success + * @param action name of the action to perform, for messages purposes. + */ + + public Builder(@Nonnull Callable callable, @Nonnull BiPredicate checkResult, @Nonnull String action) { + this.callable = callable; + this.action = action; + this.checkResult = checkResult; + } + + /** + * Create a Retrier object with the specification set in this builder. + * @return the retrier + */ + public @Nonnull Retrier build() { + return new Retrier<>(this); + } + } +} diff --git a/core/src/main/java/hudson/util/RunList.java b/core/src/main/java/hudson/util/RunList.java index d9b8adb7dc54fbb56e446337c8e75d97dc5729ea..bdc9cb61f4d6f092178150a51f2f6d0067510705 100644 --- a/core/src/main/java/hudson/util/RunList.java +++ b/core/src/main/java/hudson/util/RunList.java @@ -150,7 +150,7 @@ public class RunList extends AbstractList { public List subList(int fromIndex, int toIndex) { List r = new ArrayList(); Iterator itr = iterator(); - Iterators.skip(itr,fromIndex); + hudson.util.Iterators.skip(itr, fromIndex); for (int i=toIndex-fromIndex; i>0; i--) { r.add(itr.next()); } diff --git a/core/src/main/java/hudson/util/Secret.java b/core/src/main/java/hudson/util/Secret.java index e8380e7b04967a0733c56e06aff04dacbb50cf70..09c7c683e94ff0c70221d227e2ea75b14160f47c 100644 --- a/core/src/main/java/hudson/util/Secret.java +++ b/core/src/main/java/hudson/util/Secret.java @@ -42,6 +42,7 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.logging.Logger; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -64,6 +65,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; * @author Kohsuke Kawaguchi */ public final class Secret implements Serializable { + private static final Logger LOGGER = Logger.getLogger(Secret.class.getName()); + private static final byte PAYLOAD_V1 = 1; /** * Unencrypted secret text. @@ -92,6 +95,8 @@ public final class Secret implements Serializable { @Override @Deprecated public String toString() { + final String from = new Throwable().getStackTrace()[1].toString(); + LOGGER.warning("Use of toString() on hudson.util.Secret from "+from+". Prefer getPlainText() or getEncryptedValue() depending your needs. see https://jenkins.io/redirect/hudson.util.Secret/"); return value; } @@ -165,7 +170,7 @@ public final class Secret implements Serializable { */ @CheckForNull public static Secret decrypt(@CheckForNull String data) { - if(data==null) return null; + if(!isValidData(data)) return null; if (data.startsWith("{") && data.endsWith("}")) { //likely CBC encrypted/containing metadata but could be plain text byte[] payload; @@ -215,6 +220,16 @@ public final class Secret implements Serializable { } } + private static boolean isValidData(String data) { + if (data == null || "{}".equals(data) || "".equals(data.trim())) return false; + + if (data.startsWith("{") && data.endsWith("}")) { + return !"".equals(data.substring(1, data.length()-1).trim()); + } + + return true; + } + /** * Workaround for JENKINS-6459 / http://java.net/jira/browse/GLASSFISH-11862 * This method uses specific provider selected via hudson.util.Secret.provider system property diff --git a/core/src/main/java/hudson/util/Service.java b/core/src/main/java/hudson/util/Service.java index 6275b07e3f9f1132e07a867a5f1c34ee483ffc39..9d33249909e370cea5405ca77245e00be9e3e5c2 100644 --- a/core/src/main/java/hudson/util/Service.java +++ b/core/src/main/java/hudson/util/Service.java @@ -31,19 +31,19 @@ import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.ArrayList; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; /** - * Load classes by looking up META-INF/services. + * Load classes by looking up {@code META-INF/services}. * * @author Kohsuke Kawaguchi + * @deprecated use {@link ServiceLoader} instead. */ +@Deprecated public class Service { - /** - * Poorman's clone of JDK6 ServiceLoader. - */ public static List loadInstances(ClassLoader classLoader, Class type) throws IOException { List result = new ArrayList(); @@ -76,7 +76,7 @@ public class Service { } /** - * Look up META-INF/service/SPICLASSNAME from the classloader + * Look up {@code META-INF/service/SPICLASSNAME} from the classloader * and all the discovered classes into the given collection. */ public static void load(Class spi, ClassLoader cl, Collection> result) { diff --git a/core/src/main/java/hudson/util/StreamTaskListener.java b/core/src/main/java/hudson/util/StreamTaskListener.java index 9deea8fad0e307d90a1f0690c8ab2f59abdbfc8f..b2b2e4fd62e39e11d2764b4cc8350ea54c7566cf 100644 --- a/core/src/main/java/hudson/util/StreamTaskListener.java +++ b/core/src/main/java/hudson/util/StreamTaskListener.java @@ -24,8 +24,6 @@ package hudson.util; import hudson.CloseProofOutputStream; -import hudson.console.ConsoleNote; -import hudson.console.HudsonExceptionNote; import hudson.model.TaskListener; import hudson.remoting.RemoteOutputStream; import java.io.Closeable; @@ -34,22 +32,24 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.InvalidPathException; -import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.logging.Level; import java.util.logging.Logger; import org.kohsuke.stapler.framework.io.WriterOutputStream; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +// TODO: AbstractTaskListener is empty now, but there are dependencies on that e.g. Ruby Runtime - JENKINS-48116) +// The change needs API deprecation policy or external usages cleanup. + /** * {@link TaskListener} that generates output into a single stream. * @@ -58,8 +58,10 @@ import org.kohsuke.stapler.framework.io.WriterOutputStream; * * @author Kohsuke Kawaguchi */ -public class StreamTaskListener extends AbstractTaskListener implements Serializable, Closeable { +public class StreamTaskListener extends AbstractTaskListener implements TaskListener, Closeable { + @Nonnull private PrintStream out; + @CheckForNull private Charset charset; /** @@ -69,15 +71,15 @@ public class StreamTaskListener extends AbstractTaskListener implements Serializ * or use {@link #fromStdout()} or {@link #fromStderr()}. */ @Deprecated - public StreamTaskListener(PrintStream out) { + public StreamTaskListener(@Nonnull PrintStream out) { this(out,null); } - public StreamTaskListener(OutputStream out) { + public StreamTaskListener(@Nonnull OutputStream out) { this(out,null); } - public StreamTaskListener(OutputStream out, Charset charset) { + public StreamTaskListener(@Nonnull OutputStream out, @CheckForNull Charset charset) { try { if (charset == null) this.out = (out instanceof PrintStream) ? (PrintStream)out : new PrintStream(out, false); @@ -90,18 +92,18 @@ public class StreamTaskListener extends AbstractTaskListener implements Serializ } } - public StreamTaskListener(File out) throws IOException { + public StreamTaskListener(@Nonnull File out) throws IOException { this(out,null); } - public StreamTaskListener(File out, Charset charset) throws IOException { + public StreamTaskListener(@Nonnull File out, @CheckForNull Charset charset) throws IOException { // don't do buffering so that what's written to the listener // gets reflected to the file immediately, which can then be // served to the browser immediately this(Files.newOutputStream(asPath(out)), charset); } - private static Path asPath(File out) throws IOException { + private static Path asPath(@Nonnull File out) throws IOException { try { return out.toPath(); } catch (InvalidPathException e) { @@ -118,7 +120,7 @@ public class StreamTaskListener extends AbstractTaskListener implements Serializ * @throws IOException if the file could not be opened. * @since 1.651 */ - public StreamTaskListener(File out, boolean append, Charset charset) throws IOException { + public StreamTaskListener(@Nonnull File out, boolean append, @CheckForNull Charset charset) throws IOException { // don't do buffering so that what's written to the listener // gets reflected to the file immediately, which can then be // served to the browser immediately @@ -130,7 +132,7 @@ public class StreamTaskListener extends AbstractTaskListener implements Serializ ); } - public StreamTaskListener(Writer w) throws IOException { + public StreamTaskListener(@Nonnull Writer w) throws IOException { this(new WriterOutputStream(w)); } @@ -151,44 +153,14 @@ public class StreamTaskListener extends AbstractTaskListener implements Serializ return new StreamTaskListener(System.err,Charset.defaultCharset()); } + @Override public PrintStream getLogger() { return out; } - private PrintWriter _error(String prefix, String msg) { - out.print(prefix); - out.println(msg); - - // the idiom in Jenkins is to use the returned writer for writing stack trace, - // so put the marker here to indicate an exception. if the stack trace isn't actually written, - // HudsonExceptionNote.annotate recovers gracefully. - try { - annotate(new HudsonExceptionNote()); - } catch (IOException e) { - // for signature compatibility, we have to swallow this error - } - return new PrintWriter( - charset!=null ? new OutputStreamWriter(out,charset) : new OutputStreamWriter(out),true); - } - - public PrintWriter error(String msg) { - return _error("ERROR: ",msg); - } - - public PrintWriter error(String format, Object... args) { - return error(String.format(format,args)); - } - - public PrintWriter fatalError(String msg) { - return _error("FATAL: ",msg); - } - - public PrintWriter fatalError(String format, Object... args) { - return fatalError(String.format(format,args)); - } - - public void annotate(ConsoleNote ann) throws IOException { - ann.encodeTo(out); + @Override + public Charset getCharset() { + return charset != null ? charset : Charset.defaultCharset(); } private void writeObject(ObjectOutputStream out) throws IOException { @@ -202,6 +174,7 @@ public class StreamTaskListener extends AbstractTaskListener implements Serializ charset = name==null ? null : Charset.forName(name); } + @Override public void close() throws IOException { out.close(); } diff --git a/core/src/main/java/hudson/util/TextFile.java b/core/src/main/java/hudson/util/TextFile.java index 2cf752d43c8dcb1223f78c4d9b2a120fdd8431d0..bd6904add0960800b060048ddba96fc2d58d162d 100644 --- a/core/src/main/java/hudson/util/TextFile.java +++ b/core/src/main/java/hudson/util/TextFile.java @@ -23,22 +23,23 @@ */ package hudson.util; -import com.google.common.collect.*; +import edu.umd.cs.findbugs.annotations.CreatesObligation; + +import hudson.Util; +import jenkins.util.io.LinesStream; import java.nio.file.Files; -import java.nio.file.InvalidPathException; import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.io.Reader; import java.io.StringWriter; import java.nio.charset.Charset; -import java.util.Iterator; +import java.nio.charset.StandardCharsets; /** * Represents a text file. @@ -48,9 +49,10 @@ import java.util.Iterator; * @author Kohsuke Kawaguchi */ public class TextFile { - public final File file; - public TextFile(File file) { + public final @Nonnull File file; + + public TextFile(@Nonnull File file) { this.file = file; } @@ -68,47 +70,42 @@ public class TextFile { public String read() throws IOException { StringWriter out = new StringWriter(); PrintWriter w = new PrintWriter(out); - try (BufferedReader in = new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), "UTF-8"))) { + try (BufferedReader in = Files.newBufferedReader(Util.fileToPath(file), StandardCharsets.UTF_8)) { String line; while ((line = in.readLine()) != null) w.println(line); - } catch (InvalidPathException e) { - throw new IOException(e); + } catch (Exception e) { + throw new IOException("Failed to fully read " + file, e); } return out.toString(); } /** - * Parse text file line by line. + * @throws RuntimeException in the case of {@link IOException} in {@link #linesStream()} + * @deprecated This method does not properly propagate errors and may lead to file descriptor leaks + * if the collection is not fully iterated. Use {@link #linesStream()} instead. */ - public Iterable lines() { - return new Iterable() { - @Override - public Iterator iterator() { - try { - final BufferedReader in = new BufferedReader(new InputStreamReader( - Files.newInputStream(file.toPath()),"UTF-8")); - - return new AbstractIterator() { - @Override - protected String computeNext() { - try { - String r = in.readLine(); - if (r==null) { - in.close(); - return endOfData(); - } - return r; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }; - } catch (IOException | InvalidPathException e) { - throw new RuntimeException(e); - } - } - }; + @Deprecated + public @Nonnull Iterable lines() { + try { + return linesStream(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Creates a new {@link jenkins.util.io.LinesStream} of the file. + *

    + * Note: The caller is responsible for closing the returned + * LinesStream. + * @throws IOException if the file cannot be converted to a + * {@link java.nio.file.Path} or if the file cannot be opened for reading + * @since 2.111 + */ + @CreatesObligation + public @Nonnull LinesStream linesStream() throws IOException { + return new LinesStream(Util.fileToPath(file)); } /** diff --git a/core/src/main/java/hudson/util/TimeUnit2.java b/core/src/main/java/hudson/util/TimeUnit2.java index 30b9455fde312779a356f38e8900fb72cbd4cb30..d2aa5eb3fd27f36e20600582f55dfc6504266945 100644 --- a/core/src/main/java/hudson/util/TimeUnit2.java +++ b/core/src/main/java/hudson/util/TimeUnit2.java @@ -29,13 +29,17 @@ package hudson.util; +import hudson.RestrictedSince; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + import java.util.concurrent.TimeUnit; /** - * A TimeUnit represents time durations at a given unit of + * A {@code TimeUnit} represents time durations at a given unit of * granularity and provides utility methods to convert across units, * and to perform timing and delay operations in these units. A - * TimeUnit does not maintain time information, but only + * {@code TimeUnit} does not maintain time information, but only * helps organize and use time representations that may be maintained * separately across various contexts. A nanosecond is defined as one * thousandth of a microsecond, a microsecond as one thousandth of a @@ -43,7 +47,7 @@ import java.util.concurrent.TimeUnit; * as sixty seconds, an hour as sixty minutes, and a day as twenty four * hours. * - *

    A TimeUnit is mainly used to inform time-based methods + *

    A {@code TimeUnit} is mainly used to inform time-based methods * how a given timing parameter should be interpreted. For example, * the following code will timeout in 50 milliseconds if the {@link * java.util.concurrent.locks.Lock lock} is not available: @@ -59,11 +63,15 @@ import java.util.concurrent.TimeUnit; * * Note however, that there is no guarantee that a particular timeout * implementation will be able to notice the passage of time at the - * same granularity as the given TimeUnit. + * same granularity as the given {@code TimeUnit}. * - * @since 1.5 * @author Doug Lea + * @deprecated use {@link TimeUnit}. (Java 5 did not have all the units required, so {@link TimeUnit2} was introduced + * because it had better conversion until Java 6 went out.) */ +@Deprecated +@RestrictedSince("2.80") +@Restricted(NoExternalUse.class) public enum TimeUnit2 { NANOSECONDS { @Override public long toNanos(long d) { return d; } @@ -180,20 +188,20 @@ public enum TimeUnit2 { * Convert the given time duration in the given unit to this * unit. Conversions from finer to coarser granularities * truncate, so lose precision. For example converting - * 999 milliseconds to seconds results in - * 0. Conversions from coarser to finer granularities + * {@code 999} milliseconds to seconds results in + * {@code 0}. Conversions from coarser to finer granularities * with arguments that would numerically overflow saturate to - * Long.MIN_VALUE if negative or Long.MAX_VALUE + * {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE} * if positive. * *

    For example, to convert 10 minutes to milliseconds, use: - * TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES) + * {@code TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES)} * - * @param sourceDuration the time duration in the given sourceUnit - * @param sourceUnit the unit of the sourceDuration argument + * @param sourceDuration the time duration in the given {@code sourceUnit} + * @param sourceUnit the unit of the {@code sourceDuration} argument * @return the converted duration in this unit, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. */ public long convert(long sourceDuration, TimeUnit2 sourceUnit) { throw new AbstractMethodError(); @@ -203,31 +211,31 @@ public enum TimeUnit2 { * Convert the given time duration in the given unit to this * unit. Conversions from finer to coarser granularities * truncate, so lose precision. For example converting - * 999 milliseconds to seconds results in - * 0. Conversions from coarser to finer granularities + * {@code 999} milliseconds to seconds results in + * {@code 0}. Conversions from coarser to finer granularities * with arguments that would numerically overflow saturate to - * Long.MIN_VALUE if negative or Long.MAX_VALUE + * {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE} * if positive. * *

    For example, to convert 10 minutes to milliseconds, use: - * TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES) + * {@code TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES)} * - * @param sourceDuration the time duration in the given sourceUnit - * @param sourceUnit the unit of the sourceDuration argument + * @param sourceDuration the time duration in the given {@code sourceUnit} + * @param sourceUnit the unit of the {@code sourceDuration} argument * @return the converted duration in this unit, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. */ public long convert(long sourceDuration, TimeUnit sourceUnit) { throw new AbstractMethodError(); } /** - * Equivalent to NANOSECONDS.convert(duration, this). + * Equivalent to {@code NANOSECONDS.convert(duration, this)}. * @param duration the duration * @return the converted duration, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. * @see #convert */ public long toNanos(long duration) { @@ -235,11 +243,11 @@ public enum TimeUnit2 { } /** - * Equivalent to MICROSECONDS.convert(duration, this). + * Equivalent to {@code MICROSECONDS.convert(duration, this)}. * @param duration the duration * @return the converted duration, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. * @see #convert */ public long toMicros(long duration) { @@ -247,11 +255,11 @@ public enum TimeUnit2 { } /** - * Equivalent to MILLISECONDS.convert(duration, this). + * Equivalent to {@code MILLISECONDS.convert(duration, this)}. * @param duration the duration * @return the converted duration, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. * @see #convert */ public long toMillis(long duration) { @@ -259,11 +267,11 @@ public enum TimeUnit2 { } /** - * Equivalent to SECONDS.convert(duration, this). + * Equivalent to {@code SECONDS.convert(duration, this)}. * @param duration the duration * @return the converted duration, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. * @see #convert */ public long toSeconds(long duration) { @@ -271,37 +279,34 @@ public enum TimeUnit2 { } /** - * Equivalent to MINUTES.convert(duration, this). + * Equivalent to {@code MINUTES.convert(duration, this)}. * @param duration the duration * @return the converted duration, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. * @see #convert - * @since 1.6 */ public long toMinutes(long duration) { throw new AbstractMethodError(); } /** - * Equivalent to HOURS.convert(duration, this). + * Equivalent to {@code HOURS.convert(duration, this)}. * @param duration the duration * @return the converted duration, - * or Long.MIN_VALUE if conversion would negatively - * overflow, or Long.MAX_VALUE if it would positively overflow. + * or {@code Long.MIN_VALUE} if conversion would negatively + * overflow, or {@code Long.MAX_VALUE} if it would positively overflow. * @see #convert - * @since 1.6 */ public long toHours(long duration) { throw new AbstractMethodError(); } /** - * Equivalent to DAYS.convert(duration, this). + * Equivalent to {@code DAYS.convert(duration, this)}. * @param duration the duration * @return the converted duration * @see #convert - * @since 1.6 */ public long toDays(long duration) { throw new AbstractMethodError(); @@ -317,11 +322,11 @@ public enum TimeUnit2 { abstract int excessNanos(long d, long m); /** - * Performs a timed Object.wait using this time unit. + * Performs a timed {@code Object.wait} using this time unit. * This is a convenience method that converts timeout arguments - * into the form required by the Object.wait method. + * into the form required by the {@code Object.wait} method. * - *

    For example, you could implement a blocking poll + *

    For example, you could implement a blocking {@code poll} * method (see {@link java.util.concurrent.BlockingQueue#poll BlockingQueue.poll}) * using: * @@ -348,9 +353,9 @@ public enum TimeUnit2 { } /** - * Performs a timed Thread.join using this time unit. + * Performs a timed {@code Thread.join} using this time unit. * This is a convenience method that converts time arguments into the - * form required by the Thread.join method. + * form required by the {@code Thread.join} method. * @param thread the thread to wait for * @param timeout the maximum time to wait. If less than * or equal to zero, do not wait at all. @@ -367,9 +372,9 @@ public enum TimeUnit2 { } /** - * Performs a Thread.sleep using this unit. + * Performs a {@code Thread.sleep} using this unit. * This is a convenience method that converts time arguments into the - * form required by the Thread.sleep method. + * form required by the {@code Thread.sleep} method. * @param timeout the minimum time to sleep. If less than * or equal to zero, do not sleep at all. * @throws InterruptedException if interrupted while sleeping. diff --git a/core/src/main/java/hudson/util/UnbufferedBase64InputStream.java b/core/src/main/java/hudson/util/UnbufferedBase64InputStream.java index ee72c5c6e9aa21a468d481dfed397dfecbd8f472..36d6eb4eeb299e0b5435464046f78751d70d4536 100644 --- a/core/src/main/java/hudson/util/UnbufferedBase64InputStream.java +++ b/core/src/main/java/hudson/util/UnbufferedBase64InputStream.java @@ -1,11 +1,10 @@ package hudson.util; -import org.apache.commons.codec.binary.Base64; - import java.io.DataInputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Base64; /** * Filter InputStream that decodes base64 without doing any buffering. @@ -22,6 +21,7 @@ public class UnbufferedBase64InputStream extends FilterInputStream { private byte[] decoded; private int pos; private final DataInputStream din; + private static final Base64.Decoder decoder = Base64.getMimeDecoder(); public UnbufferedBase64InputStream(InputStream in) { super(in); @@ -38,7 +38,7 @@ public class UnbufferedBase64InputStream extends FilterInputStream { if (pos==decoded.length) { din.readFully(encoded); - decoded = Base64.decodeBase64(encoded); + decoded = decoder.decode(encoded); if (decoded.length==0) return -1; // EOF pos = 0; } diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java index dc91ccec4a805d2b9410359f185f80f444de5a30..d4124ae147442b38fe1160bd2d90bae954c6d04c 100644 --- a/core/src/main/java/hudson/util/XStream2.java +++ b/core/src/main/java/hudson/util/XStream2.java @@ -26,6 +26,7 @@ package hudson.util; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.KXml2Driver; import com.thoughtworks.xstream.mapper.AnnotationMapper; import com.thoughtworks.xstream.mapper.Mapper; import com.thoughtworks.xstream.mapper.MapperWrapper; @@ -39,17 +40,21 @@ import com.thoughtworks.xstream.converters.SingleValueConverterWrapper; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.converters.extended.DynamicProxyConverter; import com.thoughtworks.xstream.core.JVM; +import com.thoughtworks.xstream.core.util.Fields; import com.thoughtworks.xstream.io.HierarchicalStreamDriver; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.io.ReaderWrapper; import com.thoughtworks.xstream.mapper.CannotResolveClassException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.PluginManager; import hudson.PluginWrapper; +import hudson.XmlFile; import hudson.diagnosis.OldDataMonitor; import hudson.remoting.ClassFilter; import hudson.util.xstream.ImmutableSetConverter; import hudson.util.xstream.ImmutableSortedSetConverter; +import jenkins.util.xstream.SafeURLConverter; import jenkins.model.Jenkins; import hudson.model.Label; import hudson.model.Result; @@ -63,19 +68,27 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.nio.charset.Charset; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.annotation.CheckForNull; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; +import javax.annotation.Nonnull; /** * {@link XStream} enhanced for additional Java5 support and improved robustness. * @author Kohsuke Kawaguchi */ public class XStream2 extends XStream { + + private static final Logger LOGGER = Logger.getLogger(XStream2.class.getName()); + private RobustReflectionConverter reflectionConverter; private final ThreadLocal oldData = new ThreadLocal(); private final @CheckForNull ClassOwnership classOwnership; @@ -86,7 +99,18 @@ public class XStream2 extends XStream { */ private MapperInjectionPoint mapperInjectionPoint; + /** + * Convenience method so we only have to change the driver in one place + * if we switch to something new in the future + * + * @return a new instance of the HierarchicalStreamDriver we want to use + */ + public static HierarchicalStreamDriver getDefaultDriver() { + return new KXml2Driver(); + } + public XStream2() { + super(getDefaultDriver()); init(); classOwnership = null; } @@ -98,12 +122,33 @@ public class XStream2 extends XStream { } XStream2(ClassOwnership classOwnership) { + super(getDefaultDriver()); init(); this.classOwnership = classOwnership; } @Override public Object unmarshal(HierarchicalStreamReader reader, Object root, DataHolder dataHolder) { + return unmarshal(reader, root, dataHolder, false); + } + + /** + * Variant of {@link #unmarshal(HierarchicalStreamReader, Object, DataHolder)} that nulls out non-{@code transient} instance fields not defined in the source when unmarshaling into an existing object. + *

    Typically useful when loading user-supplied XML files in place (non-null {@code root}) + * where some reference-valued fields of the root object may have legitimate reasons for being null. + * Without this mode, it is impossible to clear such fields in an existing instance, + * since XStream has no notation for a null field value. + * Even for primitive-valued fields, it is useful to guarantee + * that unmarshaling will produce the same result as creating a new instance. + *

    Do not use in cases where the root objects defines fields (typically {@code final}) + * which it expects to be {@link Nonnull} unless you are prepared to restore default values for those fields. + * @param nullOut whether to perform this special behavior; + * false to use the stock XStream behavior of leaving unmentioned {@code root} fields untouched + * @see XmlFile#unmarshalNullingOut + * @see JENKINS-21017 + * @since 2.99 + */ + public Object unmarshal(HierarchicalStreamReader reader, Object root, DataHolder dataHolder, boolean nullOut) { // init() is too early to do this // defensive because some use of XStream happens before plugins are initialized. Jenkins h = Jenkins.getInstanceOrNull(); @@ -111,7 +156,54 @@ public class XStream2 extends XStream { setClassLoader(h.pluginManager.uberClassLoader); } - Object o = super.unmarshal(reader,root,dataHolder); + Object o; + if (root == null || !nullOut) { + o = super.unmarshal(reader, root, dataHolder); + } else { + Set topLevelFields = new HashSet<>(); + o = super.unmarshal(new ReaderWrapper(reader) { + int depth; + @Override + public void moveUp() { + if (--depth == 0) { + topLevelFields.add(getNodeName()); + } + super.moveUp(); + } + @Override + public void moveDown() { + try { + super.moveDown(); + } finally { + depth++; + } + } + }, root, dataHolder); + if (o == root && getConverterLookup().lookupConverterForType(o.getClass()) instanceof RobustReflectionConverter) { + getReflectionProvider().visitSerializableFields(o, (String name, Class type, Class definedIn, Object value) -> { + if (topLevelFields.contains(name)) { + return; + } + Field f = Fields.find(definedIn, name); + Object v; + if (type.isPrimitive()) { + // oddly not in com.thoughtworks.xstream.core.util.Primitives + v = ReflectionUtils.getVmDefaultValueForPrimitiveType(type); + if (v.equals(value)) { + return; + } + } else { + if (value == null) { + return; + } + v = null; + } + LOGGER.log(Level.FINE, "JENKINS-21017: nulling out {0} in {1}", new Object[] {f, o}); + Fields.write(f, o, v); + }); + } + } + if (oldData.get()!=null) { oldData.remove(); if (o instanceof Saveable) OldDataMonitor.report((Saveable)o, "1.106"); @@ -130,8 +222,8 @@ public class XStream2 extends XStream { * Specifies that a given field of a given class should not be treated with laxity by {@link RobustCollectionConverter}. * @param clazz a class which we expect to hold a non-{@code transient} field * @param field a field name in that class + * @since 2.85 this method can be used from outside core, before then it was restricted since initially added in 1.551 / 1.532.2 */ - @Restricted(NoExternalUse.class) // TODO could be opened up later public void addCriticalField(Class clazz, String field) { reflectionConverter.addCriticalField(clazz, field); } @@ -157,6 +249,8 @@ public class XStream2 extends XStream { registerConverter(new CopyOnWriteMap.Tree.ConverterImpl(getMapper()),10); // needs to override MapConverter registerConverter(new DescribableList.ConverterImpl(getMapper()),10); // explicitly added to handle subtypes registerConverter(new Label.ConverterImpl(),10); + // SECURITY-637 against URL deserialization + registerConverter(new SafeURLConverter(),10); // this should come after all the XStream's default simpler converters, // but before reflection-based one kicks in. @@ -215,7 +309,7 @@ public class XStream2 extends XStream { */ public void toXMLUTF8(Object obj, OutputStream out) throws IOException { Writer w = new OutputStreamWriter(out, Charset.forName("UTF-8")); - w.write("\n"); + w.write("\n"); toXML(obj, w); } @@ -449,27 +543,28 @@ public class XStream2 extends XStream { private static class BlacklistedTypesConverter implements Converter { @Override public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { - throw new UnsupportedOperationException("Refusing to marshal " + source.getClass().getName() + " for security reasons"); + throw new UnsupportedOperationException("Refusing to marshal " + source.getClass().getName() + " for security reasons; see https://jenkins.io/redirect/class-filter/"); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { - throw new ConversionException("Refusing to unmarshal " + reader.getNodeName() + " for security reasons"); + throw new ConversionException("Refusing to unmarshal " + reader.getNodeName() + " for security reasons; see https://jenkins.io/redirect/class-filter/"); } + /** TODO see comment in {@code whitelisted-classes.txt} */ + private static final Pattern JRUBY_PROXY = Pattern.compile("org[.]jruby[.]proxy[.].+[$]Proxy\\d+"); + @Override public boolean canConvert(Class type) { if (type == null) { return false; } - try { - ClassFilter.DEFAULT.check(type); - ClassFilter.DEFAULT.check(type.getName()); - } catch (SecurityException se) { - // claim we can convert all the scary stuff so we can throw exceptions when attempting to do so - return true; + String name = type.getName(); + if (JRUBY_PROXY.matcher(name).matches()) { + return false; } - return false; + // claim we can convert all the scary stuff so we can throw exceptions when attempting to do so + return ClassFilter.DEFAULT.isBlacklisted(name) || ClassFilter.DEFAULT.isBlacklisted(type); } } } diff --git a/core/src/main/java/hudson/util/io/ParserConfigurator.java b/core/src/main/java/hudson/util/io/ParserConfigurator.java index 878fba535b030361fdc9be6e527b1c691c621aa9..cd1b041823804a942c3f96049fe1cf5da865263e 100644 --- a/core/src/main/java/hudson/util/io/ParserConfigurator.java +++ b/core/src/main/java/hudson/util/io/ParserConfigurator.java @@ -52,7 +52,9 @@ import java.util.Collections; * * @author Kohsuke Kawaguchi * @since 1.416 + * @deprecated No longer used. */ +@Deprecated public abstract class ParserConfigurator implements ExtensionPoint, Serializable { private static final long serialVersionUID = -2523542286453177108L; @@ -78,17 +80,17 @@ public abstract class ParserConfigurator implements ExtensionPoint, Serializable if (Jenkins.getInstanceOrNull()==null) { Channel ch = Channel.current(); if (ch!=null) - all = ch.call(new SlaveToMasterCallable, IOException>() { - - private static final long serialVersionUID = -2178106894481500733L; - - public Collection call() throws IOException { - return new ArrayList(all()); - } - }); + all = ch.call(new GetParserConfigurators()); } else all = all(); for (ParserConfigurator pc : all) pc.configure(reader,context); } + private static class GetParserConfigurators extends SlaveToMasterCallable, IOException> { + private static final long serialVersionUID = -2178106894481500733L; + @Override + public Collection call() throws IOException { + return new ArrayList<>(all()); + } + } } diff --git a/core/src/main/java/hudson/util/io/TarArchiver.java b/core/src/main/java/hudson/util/io/TarArchiver.java index a223167a903dcd475101a753faf630f6f183bb15..5d97aaa43b5afba6ca42cbe067948c5bb478a8b6 100644 --- a/core/src/main/java/hudson/util/io/TarArchiver.java +++ b/core/src/main/java/hudson/util/io/TarArchiver.java @@ -102,16 +102,19 @@ final class TarArchiver extends Archiver { try { if (!file.isDirectory()) { // ensure we don't write more bytes than the declared when we created the entry - + try (InputStream fin = Files.newInputStream(file.toPath()); BoundedInputStream in = new BoundedInputStream(fin, size)) { - int len; - while ((len = in.read(buf)) >= 0) { - tar.write(buf, 0, len); + // Separate try block not to wrap exception thrown while opening the input stream into an exception + // indicating a problem while writing + try { + int len; + while ((len = in.read(buf)) >= 0) { + tar.write(buf, 0, len); + } + } catch (IOException | InvalidPathException e) {// log the exception in any case + throw new IOException("Error writing to tar file from: " + file, e); } - } catch (IOException | InvalidPathException e) {// log the exception in any case - IOException ioE = new IOException("Error writing to tar file from: " + file, e); - throw ioE; } } } finally { // always close the entry diff --git a/core/src/main/java/hudson/util/io/ZipArchiver.java b/core/src/main/java/hudson/util/io/ZipArchiver.java index 9bd58808b28315d5c7468d8ce401fda92d5ede8b..20da13cbc3a842c64586eea98333ff434cf6b134 100644 --- a/core/src/main/java/hudson/util/io/ZipArchiver.java +++ b/core/src/main/java/hudson/util/io/ZipArchiver.java @@ -30,6 +30,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.InvalidPathException; import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.Zip64Mode; import org.apache.tools.zip.ZipOutputStream; import java.io.File; @@ -49,6 +50,7 @@ final class ZipArchiver extends Archiver { ZipArchiver(OutputStream out) { zip = new ZipOutputStream(out); zip.setEncoding(System.getProperty("file.encoding")); + zip.setUseZip64(Zip64Mode.AsNeeded); } public void visit(final File f, final String _relativePath) throws IOException { diff --git a/core/src/main/java/hudson/util/jna/Kernel32Utils.java b/core/src/main/java/hudson/util/jna/Kernel32Utils.java index 08fb83c3aeb0b41a0fb331ad4a9d83011e3cab85..300c186a4c555f9c43f2779cf496f94045322e72 100644 --- a/core/src/main/java/hudson/util/jna/Kernel32Utils.java +++ b/core/src/main/java/hudson/util/jna/Kernel32Utils.java @@ -23,6 +23,8 @@ */ package hudson.util.jna; +import hudson.Util; + import java.io.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -58,6 +60,12 @@ public class Kernel32Utils { } } + /** + * @deprecated Use {@link java.nio.file.Files#readAttributes} with + * {@link java.nio.file.attribute.DosFileAttributes} and reflective calls to + * WindowsFileAttributes if necessary. + */ + @Deprecated public static int getWin32FileAttributes(File file) throws IOException { // allow lookup of paths longer than MAX_PATH // http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx @@ -85,7 +93,9 @@ public class Kernel32Utils { * If the function is not exported by kernel32. * See http://msdn.microsoft.com/en-us/library/windows/desktop/aa363866(v=vs.85).aspx * for compatibility info. + * @deprecated Use {@link Util#createSymlink} instead. */ + @Deprecated public static void createSymbolicLink(File symlink, String target, boolean dirLink) throws IOException { if (!Kernel32.INSTANCE.CreateSymbolicLinkW( new WString(symlink.getPath()), new WString(target), @@ -94,8 +104,12 @@ public class Kernel32Utils { } } + /** + * @deprecated Use {@link Util#isSymlink} to detect symbolic links and junctions instead. + */ + @Deprecated public static boolean isJunctionOrSymlink(File file) throws IOException { - return (file.exists() && (Kernel32.FILE_ATTRIBUTE_REPARSE_POINT & getWin32FileAttributes(file)) != 0); + return Util.isSymlink(file); } public static File getTempDir() { diff --git a/core/src/main/java/hudson/util/spring/package.html b/core/src/main/java/hudson/util/spring/package.html index ef210c114e8f4808a4d41910dc1bad25939b9a6b..fec56708535de67dfa7df2c39f76d320252ff68c 100644 --- a/core/src/main/java/hudson/util/spring/package.html +++ b/core/src/main/java/hudson/util/spring/package.html @@ -32,10 +32,10 @@ but modifications are made since then to make the syntax more consistent.

    Changes to the original code

    Our version has support for getting rid of surrounding "bb.beans { ... }" if the script - is parsed via the BeanBuilder.parse() method. + is parsed via the BeanBuilder.parse() method.

    - Anonymous bean definition syntax is changed to bean(CLASS) {...} from - {CLASS _ -> ...} to increase consistency with named bean definition. + Anonymous bean definition syntax is changed to bean(CLASS) {...} from + {CLASS _ -> ...} to increase consistency with named bean definition.

    \ No newline at end of file diff --git a/core/src/main/java/hudson/views/GlobalDefaultViewConfiguration.java b/core/src/main/java/hudson/views/GlobalDefaultViewConfiguration.java index c76ae404c383f3e7d98019429fc71795253bf792..36b8aea6a6dfe033e3a1d597e72274395421b501 100644 --- a/core/src/main/java/hudson/views/GlobalDefaultViewConfiguration.java +++ b/core/src/main/java/hudson/views/GlobalDefaultViewConfiguration.java @@ -41,7 +41,7 @@ public class GlobalDefaultViewConfiguration extends GlobalConfiguration { @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { // for compatibility reasons, the actual value is stored in Jenkins - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); if (json.has("primaryView")) { final String viewName = json.getString("primaryView"); final View newPrimaryView = j.getView(viewName); diff --git a/core/src/main/java/hudson/views/ListViewColumn.java b/core/src/main/java/hudson/views/ListViewColumn.java index f5fdd6e4687d961c7b801a5bab9e0a1c079382d5..4b1581a2f967743a114c48e26a722218e7a8e170 100644 --- a/core/src/main/java/hudson/views/ListViewColumn.java +++ b/core/src/main/java/hudson/views/ListViewColumn.java @@ -48,13 +48,13 @@ import net.sf.json.JSONObject; * Extension point for adding a column to a table rendering of {@link Item}s, such as {@link ListView}. * *

    - * This object must have the column.jelly. This view + * This object must have the {@code column.jelly}. This view * is called for each cell of this column. The {@link Item} object * is passed in the "job" variable. The view should render * the {@code } tag. * *

    - * This object may have an additional columnHeader.jelly. The default ColumnHeader + * This object may have an additional {@code columnHeader.jelly}. The default ColumnHeader * will render {@link #getColumnCaption()}. * *

    diff --git a/core/src/main/java/hudson/views/MyViewsTabBar.java b/core/src/main/java/hudson/views/MyViewsTabBar.java index 1d2fe44db1fc2e0b080759dabba478d4b88220d6..c3b49c268f7c7fba864476a2d89cb04eca3e1099 100644 --- a/core/src/main/java/hudson/views/MyViewsTabBar.java +++ b/core/src/main/java/hudson/views/MyViewsTabBar.java @@ -47,7 +47,7 @@ import org.kohsuke.stapler.StaplerRequest; * Extension point for adding a MyViewsTabBar header to Projects {@link MyViewsProperty}. * *

    - * This object must have the myViewTabs.jelly. This view + * This object must have the {@code myViewTabs.jelly}. This view * is called once when the My Views main panel is built. * The "views" attribute is set to the "Collection of views". * @@ -100,13 +100,13 @@ public abstract class MyViewsTabBar extends AbstractDescribableImpl - * This object must have the viewTabs.jelly. This view + * This object must have the {@code viewTabs.jelly}. This view * is called once when the project views main panel is built. * The "views" attribute is set to the "Collection of views". * @@ -102,13 +102,13 @@ public abstract class ViewsTabBar extends AbstractDescribableImpl i @Extension(ordinal=310) @Symbol("viewsTabBar") public static class GlobalConfigurationImpl extends GlobalConfiguration { public ViewsTabBar getViewsTabBar() { - return Jenkins.getInstance().getViewsTabBar(); + return Jenkins.get().getViewsTabBar(); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { // for compatibility reasons, the actual value is stored in Jenkins - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); if (json.has("viewsTabBar")) { j.setViewsTabBar(req.bindJSON(ViewsTabBar.class,json.getJSONObject("viewsTabBar"))); diff --git a/core/src/main/java/jenkins/AgentProtocol.java b/core/src/main/java/jenkins/AgentProtocol.java index 93140b71c15299190c6e16400bb8bb016dd3ab4d..587fdefa69a3fca6c0474d9360bf019547886378 100644 --- a/core/src/main/java/jenkins/AgentProtocol.java +++ b/core/src/main/java/jenkins/AgentProtocol.java @@ -8,6 +8,8 @@ import hudson.TcpSlaveAgentListener; import java.io.IOException; import java.net.Socket; import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import jenkins.model.Jenkins; /** @@ -18,6 +20,15 @@ import jenkins.model.Jenkins; * Implementations of this extension point is singleton, and its {@link #handle(Socket)} method * gets invoked concurrently whenever a new connection comes in. * + *

    Extending UI

    + *
    + *
    description.jelly
    + *
    Optional protocol description
    + *
    deprecationCause.jelly
    + *
    Optional. If the protocol is marked as {@link #isDeprecated()}, + * clarifies the deprecation reason and provides extra documentation links
    + *
    + * * @author Kohsuke Kawaguchi * @since 1.467 * @see TcpSlaveAgentListener @@ -53,6 +64,16 @@ public abstract class AgentProtocol implements ExtensionPoint { public boolean isRequired() { return false; } + + /** + * Checks if the protocol is deprecated. + * + * @since 2.75 + */ + public boolean isDeprecated() { + return false; + } + /** * Protocol name. * @@ -86,6 +107,7 @@ public abstract class AgentProtocol implements ExtensionPoint { return ExtensionList.lookup(AgentProtocol.class); } + @CheckForNull public static AgentProtocol of(String protocolName) { for (AgentProtocol p : all()) { String n = p.getName(); diff --git a/core/src/main/java/jenkins/CLI.java b/core/src/main/java/jenkins/CLI.java index 2338e0fb4ae3b958bbddd3f6e7b33501606f4f79..e0a7f38957ad990d887aac585a821bcbd14495e9 100644 --- a/core/src/main/java/jenkins/CLI.java +++ b/core/src/main/java/jenkins/CLI.java @@ -4,6 +4,8 @@ import hudson.Extension; import hudson.model.AdministrativeMonitor; import java.io.IOException; import javax.annotation.Nonnull; + +import hudson.model.PersistentDescriptor; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import org.jenkinsci.Symbol; @@ -23,7 +25,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST; */ @Restricted(NoExternalUse.class) @Extension @Symbol("remotingCLI") -public class CLI extends GlobalConfiguration { +public class CLI extends GlobalConfiguration implements PersistentDescriptor { /** * Supersedes {@link #isEnabled} if set. @@ -34,22 +36,13 @@ public class CLI extends GlobalConfiguration { @Nonnull public static CLI get() { - CLI instance = GlobalConfiguration.all().get(CLI.class); - if (instance == null) { - // should not happen - return new CLI(); - } - return instance; + return GlobalConfiguration.all().getInstance(CLI.class); } private boolean enabled = true; // historical default, but overridden in SetupWizard - public CLI() { - load(); - } - @Override - public GlobalConfigurationCategory getCategory() { + public @Nonnull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); } diff --git a/core/src/main/java/jenkins/ExtensionFilter.java b/core/src/main/java/jenkins/ExtensionFilter.java index 1f3ec5c8865f0bc0873f8c50f8315950f4a813e1..1b4b55ffda568d8920606b1d3f3358017dd09013 100644 --- a/core/src/main/java/jenkins/ExtensionFilter.java +++ b/core/src/main/java/jenkins/ExtensionFilter.java @@ -63,8 +63,9 @@ public abstract class ExtensionFilter implements ExtensionPoint { * @param type * The type of the extension that we are discovering. This is not the actual instance * type, but the contract type, such as {@link Descriptor}, {@link AdministrativeMonitor}, etc. + * @param component the actual discovered {@link hudson.Extension} object. * @return - * true to let the component into Jenkins. false to drop it and pretend + * true to let the component into Jenkins. false to drop it and pretend * as if it didn't exist. When any one of {@link ExtensionFilter}s veto * a component, it gets dropped. */ diff --git a/core/src/main/java/jenkins/FilePathFilter.java b/core/src/main/java/jenkins/FilePathFilter.java index bb6a47a9f90fbaaba1efdd30b35c7d55566244aa..4d4eb4f2be2f0af8cbb495fbca9ebc6fe967348a 100644 --- a/core/src/main/java/jenkins/FilePathFilter.java +++ b/core/src/main/java/jenkins/FilePathFilter.java @@ -23,7 +23,7 @@ import java.io.File; * * @author Kohsuke Kawaguchi * @see FilePath - * @since 1.THU + * @since 1.587 / 1.580.1 */ public abstract class FilePathFilter { /** @@ -102,7 +102,7 @@ public abstract class FilePathFilter { /** * Returns an {@link FilePathFilter} object that represents all the in-scope filters, - * or null if none is needed. + * or {@code null} if none is needed. */ public static @CheckForNull FilePathFilter current() { Channel ch = Channel.current(); diff --git a/core/src/main/java/jenkins/FilePathFilterAggregator.java b/core/src/main/java/jenkins/FilePathFilterAggregator.java index 8bf4949e17fdcf2e56443fef5fabdcc923b8127e..f81ce2f77f55ce39c9ff1e518ba4c9704e616457 100644 --- a/core/src/main/java/jenkins/FilePathFilterAggregator.java +++ b/core/src/main/java/jenkins/FilePathFilterAggregator.java @@ -15,7 +15,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * * @author Kohsuke Kawaguchi * @see FilePath - * @since 1.THU + * @since 1.587 / 1.580.1 */ class FilePathFilterAggregator extends FilePathFilter { private final CopyOnWriteArrayList all = new CopyOnWriteArrayList(); diff --git a/core/src/main/java/jenkins/InitReactorRunner.java b/core/src/main/java/jenkins/InitReactorRunner.java index c5c2bfb844484ba8bff5b6c0f76c082581fbda45..aa6ef83ab752d36d827f277f02a1c31ab6376419 100644 --- a/core/src/main/java/jenkins/InitReactorRunner.java +++ b/core/src/main/java/jenkins/InitReactorRunner.java @@ -1,11 +1,11 @@ package jenkins; +import com.google.common.collect.Lists; import jenkins.util.SystemProperties; import hudson.init.InitMilestone; import hudson.init.InitReactorListener; import hudson.util.DaemonThreadFactory; import hudson.util.NamingThreadFactory; -import hudson.util.Service; import jenkins.model.Configuration; import jenkins.model.Jenkins; import org.jvnet.hudson.reactor.Milestone; @@ -16,6 +16,7 @@ import org.jvnet.hudson.reactor.Task; import java.io.IOException; import java.util.List; +import java.util.ServiceLoader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; @@ -59,7 +60,7 @@ public class InitReactorRunner { * As such there's no way for plugins to participate into this process. */ private ReactorListener buildReactorListener() throws IOException { - List r = (List) Service.loadInstances(Thread.currentThread().getContextClassLoader(), InitReactorListener.class); + List r = Lists.newArrayList(ServiceLoader.load(InitReactorListener.class, Thread.currentThread().getContextClassLoader())); r.add(new ReactorListener() { final Level level = Level.parse( Configuration.getStringConfigParameter("initLogLevel", "FINE") ); public void onTaskStarted(Task t) { diff --git a/core/src/main/java/jenkins/MasterToSlaveFileCallable.java b/core/src/main/java/jenkins/MasterToSlaveFileCallable.java index cad6813c180510996d29c9d7e3b01118d8ae5cea..ce7d8df28ef5c2e77a6ed2fb63040047c37fced5 100644 --- a/core/src/main/java/jenkins/MasterToSlaveFileCallable.java +++ b/core/src/main/java/jenkins/MasterToSlaveFileCallable.java @@ -1,13 +1,21 @@ package jenkins; import hudson.FilePath.FileCallable; +import hudson.remoting.VirtualChannel; import jenkins.security.Roles; +import jenkins.slaves.RemotingVersionInfo; import org.jenkinsci.remoting.RoleChecker; +import java.io.File; + /** * {@link FileCallable}s that are meant to be only used on the master. * - * @since 1.THU + * Note that the logic within {@link #invoke(File, VirtualChannel)} should use API of a minimum supported Remoting version. + * See {@link RemotingVersionInfo#getMinimumSupportedVersion()}. + * + * @since 1.587 / 1.580.1 + * @param the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist */ public abstract class MasterToSlaveFileCallable implements FileCallable { @Override diff --git a/core/src/main/java/jenkins/ReflectiveFilePathFilter.java b/core/src/main/java/jenkins/ReflectiveFilePathFilter.java index ce963943cdafe8b7438a1f98a81d61bd3898697b..f35186aebc1c83e8135a00e61509a1c5aad253b9 100644 --- a/core/src/main/java/jenkins/ReflectiveFilePathFilter.java +++ b/core/src/main/java/jenkins/ReflectiveFilePathFilter.java @@ -7,7 +7,7 @@ import java.io.File; * operations as a single string argument. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public abstract class ReflectiveFilePathFilter extends FilePathFilter { /** diff --git a/core/src/main/java/jenkins/SlaveToMasterFileCallable.java b/core/src/main/java/jenkins/SlaveToMasterFileCallable.java index bda39cff47f8e9bfb88538f768e0b041e7dfc8bb..236bd738f2cc0c78ed68af9d3c3f91093ab343be 100644 --- a/core/src/main/java/jenkins/SlaveToMasterFileCallable.java +++ b/core/src/main/java/jenkins/SlaveToMasterFileCallable.java @@ -6,8 +6,8 @@ import org.jenkinsci.remoting.RoleChecker; /** * {@link FileCallable}s that can be executed on the master, sent by the agent. - * - * @since 1.THU + * Note that any serializable fields must either be defined in your plugin or included in the stock JEP-200 whitelist. + * @since 1.587 / 1.580.1 */ public abstract class SlaveToMasterFileCallable implements FileCallable { @Override diff --git a/core/src/main/java/jenkins/SoloFilePathFilter.java b/core/src/main/java/jenkins/SoloFilePathFilter.java index ce135759824434df1e830e7c44556649c2060075..aa0ebbefc8592ec60fe1a27b0edb5e50cccc74dd 100644 --- a/core/src/main/java/jenkins/SoloFilePathFilter.java +++ b/core/src/main/java/jenkins/SoloFilePathFilter.java @@ -1,5 +1,7 @@ package jenkins; +import hudson.FilePath; + import javax.annotation.Nullable; import java.io.File; @@ -31,39 +33,43 @@ public final class SoloFilePathFilter extends FilePathFilter { throw new SecurityException("agent may not " + op + " " + f+"\nSee https://jenkins.io/redirect/security-144 for more details"); return true; } + + private File normalize(File file){ + return new File(FilePath.normalize(file.getAbsolutePath())); + } @Override public boolean read(File f) throws SecurityException { - return noFalse("read",f,base.read(f)); + return noFalse("read",f,base.read(normalize(f))); } @Override public boolean write(File f) throws SecurityException { - return noFalse("write",f,base.write(f)); + return noFalse("write",f,base.write(normalize(f))); } @Override public boolean symlink(File f) throws SecurityException { - return noFalse("symlink",f,base.write(f)); + return noFalse("symlink",f,base.write(normalize(f))); } @Override public boolean mkdirs(File f) throws SecurityException { - return noFalse("mkdirs",f,base.mkdirs(f)); + return noFalse("mkdirs",f,base.mkdirs(normalize(f))); } @Override public boolean create(File f) throws SecurityException { - return noFalse("create",f,base.create(f)); + return noFalse("create",f,base.create(normalize(f))); } @Override public boolean delete(File f) throws SecurityException { - return noFalse("delete",f,base.delete(f)); + return noFalse("delete",f,base.delete(normalize(f))); } @Override public boolean stat(File f) throws SecurityException { - return noFalse("stat",f,base.stat(f)); + return noFalse("stat",f,base.stat(normalize(f))); } } diff --git a/core/src/main/java/jenkins/diagnosis/HsErrPidList.java b/core/src/main/java/jenkins/diagnosis/HsErrPidList.java index da7f0cde60ec1375e4f6a9ef6fb1d015bac9e2e3..c38bdb06f4ed4b10c39b204f319afaaf944fd8d3 100644 --- a/core/src/main/java/jenkins/diagnosis/HsErrPidList.java +++ b/core/src/main/java/jenkins/diagnosis/HsErrPidList.java @@ -11,6 +11,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; import jenkins.model.Jenkins; +import jenkins.security.stapler.StaplerDispatchable; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileSet; @@ -94,6 +95,7 @@ public class HsErrPidList extends AdministrativeMonitor { /** * Expose files to the URL. */ + @StaplerDispatchable public List getFiles() { return files; } diff --git a/core/src/main/java/jenkins/diagnostics/RootUrlNotSetMonitor.java b/core/src/main/java/jenkins/diagnostics/RootUrlNotSetMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..bb4a252409d7bd10cce9091e2c6eda73c1a5058f --- /dev/null +++ b/core/src/main/java/jenkins/diagnostics/RootUrlNotSetMonitor.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.diagnostics; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import jenkins.model.JenkinsLocationConfiguration; +import jenkins.util.UrlHelper; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Jenkins root URL is required for a lot of operations in both core and plugins. + * There is a default behavior (infer the URL from the request object), but inaccurate in some scenarios. + * Normally this root URL is set during SetupWizard phase, this monitor is there to ensure that behavior. + * Potential exceptions are the dev environment, if someone disable the wizard or + * the administrator put an empty string on the configuration page. + * + * @since 2.119 + */ +@Extension +@Symbol("rootUrlNotSet") +@Restricted(NoExternalUse.class) +public class RootUrlNotSetMonitor extends AdministrativeMonitor { + @Override + public String getDisplayName() { + return Messages.RootUrlNotSetMonitor_DisplayName(); + } + + @Override + public boolean isActivated() { + JenkinsLocationConfiguration loc = JenkinsLocationConfiguration.get(); + return loc.getUrl() == null || !UrlHelper.isValidRootUrl(loc.getUrl()); + } + + // used by jelly to determined if it's a null url or invalid one + @Restricted(NoExternalUse.class) + public boolean isUrlNull(){ + JenkinsLocationConfiguration loc = JenkinsLocationConfiguration.get(); + return loc.getUrl() == null; + } +} diff --git a/core/src/main/java/jenkins/install/InstallState.java b/core/src/main/java/jenkins/install/InstallState.java index 71790d3c5483012c7f959afdd3c5a8f166943d31..4f7798c1ed3ca44f3bab1cb0870c537731e97a9b 100644 --- a/core/src/main/java/jenkins/install/InstallState.java +++ b/core/src/main/java/jenkins/install/InstallState.java @@ -32,6 +32,8 @@ import hudson.ExtensionPoint; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; +import jenkins.security.stapler.StaplerAccessibleType; import org.apache.commons.lang.StringUtils; /** * Jenkins install state. @@ -44,12 +46,44 @@ import org.apache.commons.lang.StringUtils; * * @author tom.fennelly@gmail.com */ +@StaplerAccessibleType public class InstallState implements ExtensionPoint { + + /** + * Only here for XStream compatibility.

    + * + * Please DO NOT ADD ITEM TO THIS LIST.

    + * If you add an item here, the deserialization process will break + * because it is used for serialized state like "jenkins.install.InstallState$4" + * before the change from anonymous class to named class. If you need to add a new InstallState, you can just add a new inner named class but nothing to change in this list. + * + * @see #readResolve + */ + @Deprecated + @SuppressWarnings("MismatchedReadAndWriteOfArray") + private static final InstallState[] UNUSED_INNER_CLASSES = { + new InstallState("UNKNOWN", false) {}, + new InstallState("INITIAL_SETUP_COMPLETED", false) {}, + new InstallState("CREATE_ADMIN_USER", false) {}, + new InstallState("INITIAL_SECURITY_SETUP", false) {}, + new InstallState("RESTART", false) {}, + new InstallState("DOWNGRADE", false) {}, + }; + /** * Need InstallState != NEW for tests by default */ @Extension - public static final InstallState UNKNOWN = new InstallState("UNKNOWN", true); + public static final InstallState UNKNOWN = new Unknown(); + private static class Unknown extends InstallState { + Unknown() { + super("UNKNOWN", true); + } + @Override + public void initializeState() { + InstallUtil.proceedToNextStateFrom(this); + } + } /** * After any setup / restart / etc. hooks are done, states should be running @@ -61,9 +95,13 @@ public class InstallState implements ExtensionPoint { * The initial set up has been completed */ @Extension - public static final InstallState INITIAL_SETUP_COMPLETED = new InstallState("INITIAL_SETUP_COMPLETED", true) { + public static final InstallState INITIAL_SETUP_COMPLETED = new InitialSetupCompleted(); + private static final class InitialSetupCompleted extends InstallState { + InitialSetupCompleted() { + super("INITIAL_SETUP_COMPLETED", true); + } public void initializeState() { - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); try { j.getSetupWizard().completeSetup(); } catch (Exception e) { @@ -71,22 +109,41 @@ public class InstallState implements ExtensionPoint { } j.setInstallState(RUNNING); } - }; + } /** * Creating an admin user for an initial Jenkins install. */ @Extension - public static final InstallState CREATE_ADMIN_USER = new InstallState("CREATE_ADMIN_USER", false) { + public static final InstallState CREATE_ADMIN_USER = new CreateAdminUser(); + private static final class CreateAdminUser extends InstallState { + CreateAdminUser() { + super("CREATE_ADMIN_USER", false); + } public void initializeState() { - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); // Skip this state if not using the security defaults // e.g. in an init script set up security already if (!j.getSetupWizard().isUsingSecurityDefaults()) { InstallUtil.proceedToNextStateFrom(this); } } - }; + } + + @Extension + public static final InstallState CONFIGURE_INSTANCE = new ConfigureInstance(); + private static final class ConfigureInstance extends InstallState { + ConfigureInstance() { + super("CONFIGURE_INSTANCE", false); + } + public void initializeState() { + // Skip this state if a boot script already configured the root URL + // in case we add more fields in this page, this should be adapted + if (StringUtils.isNotBlank(JenkinsLocationConfiguration.getOrDie().getUrl())) { + InstallUtil.proceedToNextStateFrom(this); + } + } + } /** * New Jenkins install. The user has kicked off the process of installing an @@ -94,38 +151,46 @@ public class InstallState implements ExtensionPoint { */ @Extension public static final InstallState INITIAL_PLUGINS_INSTALLING = new InstallState("INITIAL_PLUGINS_INSTALLING", false); - + /** * Security setup for a new Jenkins install. */ @Extension - public static final InstallState INITIAL_SECURITY_SETUP = new InstallState("INITIAL_SECURITY_SETUP", false) { + public static final InstallState INITIAL_SECURITY_SETUP = new InitialSecuritySetup(); + private static final class InitialSecuritySetup extends InstallState { + InitialSecuritySetup() { + super("INITIAL_SECURITY_SETUP", false); + } public void initializeState() { try { - Jenkins.getInstance().getSetupWizard().init(true); + Jenkins.get().getSetupWizard().init(true); } catch (Exception e) { throw new RuntimeException(e); } - + InstallUtil.proceedToNextStateFrom(INITIAL_SECURITY_SETUP); } - }; + } /** * New Jenkins install. */ @Extension public static final InstallState NEW = new InstallState("NEW", false); - + /** * Restart of an existing Jenkins install. */ @Extension - public static final InstallState RESTART = new InstallState("RESTART", true) { + public static final InstallState RESTART = new Restart(); + private static final class Restart extends InstallState { + Restart() { + super("RESTART", true); + } public void initializeState() { InstallUtil.saveLastExecVersion(); } - }; + } /** * Upgrade of an existing Jenkins install. @@ -137,11 +202,15 @@ public class InstallState implements ExtensionPoint { * Downgrade of an existing Jenkins install. */ @Extension - public static final InstallState DOWNGRADE = new InstallState("DOWNGRADE", true) { + public static final InstallState DOWNGRADE = new Downgrade(); + private static final class Downgrade extends InstallState { + Downgrade() { + super("DOWNGRADE", true); + } public void initializeState() { InstallUtil.saveLastExecVersion(); } - }; + } private static final Logger LOGGER = Logger.getLogger(InstallState.class.getName()); @@ -156,7 +225,11 @@ public class InstallState implements ExtensionPoint { */ public static final InstallState DEVELOPMENT = new InstallState("DEVELOPMENT", true); - private final boolean isSetupComplete; + private final transient boolean isSetupComplete; + + /** + * Link with the pluginSetupWizardGui.js map: "statsHandlers" + */ private final String name; public InstallState(@Nonnull String name, boolean isSetupComplete) { @@ -169,8 +242,16 @@ public class InstallState implements ExtensionPoint { */ public void initializeState() { } - - public Object readResolve() { + + /** + * The actual class name is irrelevant; this is functionally an enum. + *

    Creating a {@code writeReplace} does not help much since XStream then just saves: + * {@code } + * @see #UNUSED_INNER_CLASSES + * @deprecated Should no longer be used, as {@link Jenkins} now saves only {@link #name}. + */ + @Deprecated + protected Object readResolve() { // If we get invalid state from the configuration, fallback to unknown if (StringUtils.isBlank(name)) { LOGGER.log(Level.WARNING, "Read install state with blank name: ''{0}''. It will be ignored", name); diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index 9f2a150f9158f5f815526f02f86901ed17198d33..7552f9fa35ea5f43b4be51cfe5c7f6ead95c61bf 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -54,6 +54,7 @@ import hudson.model.UpdateCenter.DownloadJob.Installing; import hudson.model.UpdateCenter.InstallationJob; import hudson.model.UpdateCenter.UpdateCenterJob; import hudson.util.VersionNumber; +import java.util.logging.Level; import jenkins.model.Jenkins; import jenkins.util.SystemProperties; import jenkins.util.xml.XMLUtils; @@ -69,7 +70,8 @@ public class InstallUtil { private static final Logger LOGGER = Logger.getLogger(InstallUtil.class.getName()); // tests need this to be 1.0 - private static final VersionNumber NEW_INSTALL_VERSION = new VersionNumber("1.0"); + @Restricted(NoExternalUse.class) + public static final VersionNumber NEW_INSTALL_VERSION = new VersionNumber("1.0"); private static final VersionNumber FORCE_NEW_INSTALL_VERSION = new VersionNumber("0.0"); /** @@ -91,7 +93,6 @@ public class InstallUtil { */ public static void proceedToNextStateFrom(InstallState prior) { InstallState next = getNextInstallState(prior); - if (Main.isDevelopmentMode) LOGGER.info("Install state transitioning from: " + prior + " to: " + next); if (next != null) { Jenkins.getInstance().setInstallState(next); } @@ -100,37 +101,30 @@ public class InstallUtil { /** * Returns the next state during a transition from the current install state */ - /*package*/ static InstallState getNextInstallState(final InstallState current) { + /*package*/ static InstallState getNextInstallState(InstallState current) { List,InstallState>> installStateFilterChain = new ArrayList<>(); - for (final InstallStateFilter setupExtension : InstallStateFilter.all()) { - installStateFilterChain.add(new Function, InstallState>() { - @Override - public InstallState apply(Provider next) { - return setupExtension.getNextInstallState(current, next); - } - }); + for (InstallStateFilter setupExtension : InstallStateFilter.all()) { + installStateFilterChain.add(next -> setupExtension.getNextInstallState(current, next)); } // Terminal condition: getNextState() on the current install state - installStateFilterChain.add(new Function, InstallState>() { - @Override - public InstallState apply(Provider input) { - // Initially, install state is unknown and - // needs to be determined - if (current == null || InstallState.UNKNOWN.equals(current)) { - return getDefaultInstallState(); - } - final Map states = new HashMap(); - { - states.put(InstallState.CREATE_ADMIN_USER, InstallState.INITIAL_SETUP_COMPLETED); - states.put(InstallState.INITIAL_PLUGINS_INSTALLING, InstallState.CREATE_ADMIN_USER); - states.put(InstallState.INITIAL_SECURITY_SETUP, InstallState.NEW); - states.put(InstallState.RESTART, InstallState.RUNNING); - states.put(InstallState.UPGRADE, InstallState.INITIAL_SETUP_COMPLETED); - states.put(InstallState.DOWNGRADE, InstallState.INITIAL_SETUP_COMPLETED); - states.put(InstallState.INITIAL_SETUP_COMPLETED, InstallState.RUNNING); - } - return states.get(current); + installStateFilterChain.add(input -> { + // Initially, install state is unknown and + // needs to be determined + if (current == null || InstallState.UNKNOWN.equals(current)) { + return getDefaultInstallState(); + } + Map states = new HashMap<>(); + { + states.put(InstallState.CONFIGURE_INSTANCE, InstallState.INITIAL_SETUP_COMPLETED); + states.put(InstallState.CREATE_ADMIN_USER, InstallState.CONFIGURE_INSTANCE); + states.put(InstallState.INITIAL_PLUGINS_INSTALLING, InstallState.CREATE_ADMIN_USER); + states.put(InstallState.INITIAL_SECURITY_SETUP, InstallState.NEW); + states.put(InstallState.RESTART, InstallState.RUNNING); + states.put(InstallState.UPGRADE, InstallState.INITIAL_SETUP_COMPLETED); + states.put(InstallState.DOWNGRADE, InstallState.INITIAL_SETUP_COMPLETED); + states.put(InstallState.INITIAL_SETUP_COMPLETED, InstallState.RUNNING); } + return states.get(current); }); ProviderChain chain = new ProviderChain<>(installStateFilterChain.iterator()); @@ -256,6 +250,7 @@ public class InstallUtil { try { String lastVersion = XMLUtils.getValue("/hudson/version", configFile); if (lastVersion.length() > 0) { + LOGGER.log(Level.FINE, "discovered serialized lastVersion {0}", lastVersion); return lastVersion; } } catch (Exception e) { diff --git a/core/src/main/java/jenkins/install/SetupWizard.java b/core/src/main/java/jenkins/install/SetupWizard.java index 992ce935a7696d13dc0ef0d56b9a6e03de9d08ae..44d709030d0de2ec02ac4c2ef0a2533fc2c5d9f0 100644 --- a/core/src/main/java/jenkins/install/SetupWizard.java +++ b/core/src/main/java/jenkins/install/SetupWizard.java @@ -4,7 +4,9 @@ import static org.apache.commons.io.FileUtils.readFileToString; import static org.apache.commons.lang.StringUtils.defaultIfBlank; import java.io.IOException; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -19,8 +21,12 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import jenkins.model.JenkinsLocationConfiguration; +import jenkins.security.seed.UserSeedProperty; import jenkins.util.SystemProperties; +import jenkins.util.UrlHelper; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; @@ -28,6 +34,7 @@ import org.acegisecurity.userdetails.UsernameNotFoundException; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -39,6 +46,7 @@ import hudson.model.PageDecorator; import hudson.model.UpdateCenter; import hudson.model.UpdateSite; import hudson.model.User; +import hudson.security.AccountCreationFailedException; import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; import hudson.security.HudsonPrivateSecurityRealm; import hudson.security.SecurityRealm; @@ -52,6 +60,8 @@ import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; +import java.util.Arrays; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import jenkins.CLI; @@ -74,6 +84,10 @@ import org.kohsuke.stapler.interceptor.RequirePOST; @Restricted(NoExternalUse.class) @Extension public class SetupWizard extends PageDecorator { + public SetupWizard() { + checkFilter(); + } + /** * The security token parameter name */ @@ -81,16 +95,11 @@ public class SetupWizard extends PageDecorator { private static final Logger LOGGER = Logger.getLogger(SetupWizard.class.getName()); - /** - * Used to determine if this was a new install (vs. an upgrade, restart, or otherwise) - */ - private static boolean isUsingSecurityToken = false; - /** * Initialize the setup wizard, this will process any current state initializations */ /*package*/ void init(boolean newInstall) throws IOException, InterruptedException { - Jenkins jenkins = Jenkins.getInstance(); + Jenkins jenkins = Jenkins.get(); if(newInstall) { // this was determined to be a new install, don't run the update wizard here @@ -126,9 +135,9 @@ public class SetupWizard extends PageDecorator { // Disable CLI over Remoting CLI.get().setEnabled(false); - + // require a crumb issuer - jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false)); + jenkins.setCrumbIssuer(new DefaultCrumbIssuer(SystemProperties.getBoolean(Jenkins.class.getName() + ".crumbIssuerProxyCompatibility",false))); // set master -> slave security: jenkins.getInjector().getInstance(AdminWhitelistRule.class) @@ -158,17 +167,8 @@ public class SetupWizard extends PageDecorator { + "*************************************************************" + ls + "*************************************************************" + ls); } - - try { - PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER); - // if we're not using security defaults, we should not show the security token screen - // users will likely be sent to a login screen instead - isUsingSecurityToken = isUsingSecurityDefaults(); - } catch (ServletException e) { - throw new RuntimeException("Unable to add PluginServletFilter for the SetupWizard", e); - } } - + try { // Make sure plugin metadata is up to date UpdateCenter.updateDefaultSite(); @@ -176,14 +176,34 @@ public class SetupWizard extends PageDecorator { LOGGER.log(Level.WARNING, e.getMessage(), e); } } - + + private void setUpFilter() { + try { + if (!PluginServletFilter.hasFilter(FORCE_SETUP_WIZARD_FILTER)) { + PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER); + } + } catch (ServletException e) { + throw new RuntimeException("Unable to add PluginServletFilter for the SetupWizard", e); + } + } + + private void tearDownFilter() { + try { + if (PluginServletFilter.hasFilter(FORCE_SETUP_WIZARD_FILTER)) { + PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER); + } + } catch (ServletException e) { + throw new RuntimeException("Unable to remove PluginServletFilter for the SetupWizard", e); + } + } + /** * Indicates a generated password should be used - e.g. this is a new install, no security realm set up */ + @SuppressWarnings("unused") // used by jelly public boolean isUsingSecurityToken() { try { - return isUsingSecurityToken // only ever show the unlock page if using the security token - && !Jenkins.getInstance().getInstallState().isSetupComplete() + return !Jenkins.get().getInstallState().isSetupComplete() && isUsingSecurityDefaults(); } catch (Exception e) { // ignore @@ -197,7 +217,7 @@ public class SetupWizard extends PageDecorator { * Other settings are irrelevant. */ /*package*/ boolean isUsingSecurityDefaults() { - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); if (j.getSecurityRealm() instanceof HudsonPrivateSecurityRealm) { HudsonPrivateSecurityRealm securityRealm = (HudsonPrivateSecurityRealm)j.getSecurityRealm(); try { @@ -221,52 +241,116 @@ public class SetupWizard extends PageDecorator { * Called during the initial setup to create an admin user */ @RequirePOST - public HttpResponse doCreateAdminUser(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + @Restricted(NoExternalUse.class) + public HttpResponse doCreateAdminUser(StaplerRequest req, StaplerResponse rsp) throws IOException { Jenkins j = Jenkins.getInstance(); + j.checkPermission(Jenkins.ADMINISTER); - + // This will be set up by default. if not, something changed, ok to fail - HudsonPrivateSecurityRealm securityRealm = (HudsonPrivateSecurityRealm)j.getSecurityRealm(); - + HudsonPrivateSecurityRealm securityRealm = (HudsonPrivateSecurityRealm) j.getSecurityRealm(); + User admin = securityRealm.getUser(SetupWizard.initialSetupAdminUserName); try { - if(admin != null) { + if (admin != null) { admin.delete(); // assume the new user may well be 'admin' } + + User newUser = securityRealm.createAccountFromSetupWizard(req); + if (admin != null) { + admin = null; + } + + // Success! Delete the temporary password file: + try { + getInitialAdminPasswordFile().delete(); + } catch (InterruptedException e) { + throw new IOException(e); + } + + InstallUtil.proceedToNextStateFrom(InstallState.CREATE_ADMIN_USER); + + // ... and then login + Authentication auth = new UsernamePasswordAuthenticationToken(newUser.getId(), req.getParameter("password1")); + auth = securityRealm.getSecurityComponents().manager.authenticate(auth); + SecurityContextHolder.getContext().setAuthentication(auth); - User u = securityRealm.createAccountByAdmin(req, rsp, "/jenkins/install/SetupWizard/setupWizardFirstUser.jelly", null); - if (u != null) { - if(admin != null) { - admin = null; - } - - // Success! Delete the temporary password file: - try { - getInitialAdminPasswordFile().delete(); - } catch (InterruptedException e) { - throw new IOException(e); - } - - InstallUtil.proceedToNextStateFrom(InstallState.CREATE_ADMIN_USER); - - // ... and then login - Authentication a = new UsernamePasswordAuthenticationToken(u.getId(),req.getParameter("password1")); - a = securityRealm.getSecurityComponents().manager.authenticate(a); - SecurityContextHolder.getContext().setAuthentication(a); - CrumbIssuer crumbIssuer = Jenkins.getInstance().getCrumbIssuer(); - JSONObject data = new JSONObject(); - if (crumbIssuer != null) { - data.accumulate("crumbRequestField", crumbIssuer.getCrumbRequestField()).accumulate("crumb", crumbIssuer.getCrumb(req)); - } - return HttpResponses.okJSON(data); - } else { - return HttpResponses.okJSON(); + HttpSession session = req.getSession(false); + if (session != null) { + // avoid session fixation + session.invalidate(); } + HttpSession newSession = req.getSession(true); + + UserSeedProperty userSeed = newUser.getProperty(UserSeedProperty.class); + String sessionSeed = userSeed.getSeed(); + // include the new seed + newSession.setAttribute(UserSeedProperty.USER_SESSION_SEED, sessionSeed); + + CrumbIssuer crumbIssuer = Jenkins.getInstance().getCrumbIssuer(); + JSONObject data = new JSONObject(); + if (crumbIssuer != null) { + data.accumulate("crumbRequestField", crumbIssuer.getCrumbRequestField()).accumulate("crumb", crumbIssuer.getCrumb(req)); + } + return HttpResponses.okJSON(data); + } catch (AccountCreationFailedException e) { + /* + Return Unprocessable Entity from WebDAV. While this is not technically in the HTTP/1.1 standard, browsers + seem to accept this. 400 Bad Request is technically inappropriate because that implies invalid *syntax*, + not incorrect data. The client only cares about it being >200 anyways. + */ + rsp.setStatus(422); + return HttpResponses.forwardToView(securityRealm, "/jenkins/install/SetupWizard/setupWizardFirstUser.jelly"); } finally { - if(admin != null) { + if (admin != null) { admin.save(); // recreate this initial user if something failed } } + } + + @RequirePOST + @Restricted(NoExternalUse.class) + public HttpResponse doConfigureInstance(StaplerRequest req, @QueryParameter String rootUrl) { + Jenkins.get().checkPermission(Jenkins.ADMINISTER); + + Map errors = new HashMap<>(); + // pre-check data + checkRootUrl(errors, rootUrl); + + if(!errors.isEmpty()){ + return HttpResponses.errorJSON(Messages.SetupWizard_ConfigureInstance_ValidationErrors(), errors); + } + + // use the parameters to configure the instance + useRootUrl(errors, rootUrl); + + if(!errors.isEmpty()){ + return HttpResponses.errorJSON(Messages.SetupWizard_ConfigureInstance_ValidationErrors(), errors); + } + + InstallUtil.proceedToNextStateFrom(InstallState.CONFIGURE_INSTANCE); + + CrumbIssuer crumbIssuer = Jenkins.get().getCrumbIssuer(); + JSONObject data = new JSONObject(); + if (crumbIssuer != null) { + data.accumulate("crumbRequestField", crumbIssuer.getCrumbRequestField()).accumulate("crumb", crumbIssuer.getCrumb(req)); + } + return HttpResponses.okJSON(data); + } + + private void checkRootUrl(Map errors, @CheckForNull String rootUrl){ + if(rootUrl == null){ + errors.put("rootUrl", Messages.SetupWizard_ConfigureInstance_RootUrl_Empty()); + return; + } + if(!UrlHelper.isValidRootUrl(rootUrl)){ + errors.put("rootUrl", Messages.SetupWizard_ConfigureInstance_RootUrl_Invalid()); + } + } + + private void useRootUrl(Map errors, @CheckForNull String rootUrl){ + LOGGER.log(Level.FINE, "Root URL set during SetupWizard to {0}", new Object[]{ rootUrl }); + JenkinsLocationConfiguration.getOrDie().setUrl(rootUrl); } /*package*/ void setCurrentLevel(VersionNumber v) throws IOException { @@ -279,7 +363,7 @@ public class SetupWizard extends PageDecorator { * This file records the version number that the installation has upgraded to. */ /*package*/ static File getUpdateStateFile() { - return new File(Jenkins.getInstance().getRootDir(),"jenkins.install.UpgradeWizard.state"); + return new File(Jenkins.get().getRootDir(),"jenkins.install.UpgradeWizard.state"); } /** @@ -308,9 +392,9 @@ public class SetupWizard extends PageDecorator { */ @Restricted(DoNotUse.class) // WebOnly public HttpResponse doPlatformPluginList() throws IOException { - jenkins.install.SetupWizard setupWizard = Jenkins.getInstance().getSetupWizard(); + SetupWizard setupWizard = Jenkins.get().getSetupWizard(); if (setupWizard != null) { - if (InstallState.UPGRADE.equals(Jenkins.getInstance().getInstallState())) { + if (InstallState.UPGRADE.equals(Jenkins.get().getInstallState())) { JSONArray initialPluginData = getPlatformPluginUpdates(); if(initialPluginData != null) { return HttpResponses.okJSON(initialPluginData); @@ -332,7 +416,7 @@ public class SetupWizard extends PageDecorator { @Restricted(DoNotUse.class) // WebOnly public HttpResponse doRestartStatus() throws IOException { JSONObject response = new JSONObject(); - Jenkins jenkins = Jenkins.getInstance(); + Jenkins jenkins = Jenkins.get(); response.put("restartRequired", jenkins.getUpdateCenter().isRestartRequiredForCompletion()); response.put("restartSupported", jenkins.getLifecycle().canRestart()); return HttpResponses.okJSON(response); @@ -358,9 +442,9 @@ public class SetupWizard extends PageDecorator { */ @CheckForNull /*package*/ JSONArray getPlatformPluginList() { - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.get().checkPermission(Jenkins.ADMINISTER); JSONArray initialPluginList = null; - updateSiteList: for (UpdateSite updateSite : Jenkins.getInstance().getUpdateCenter().getSiteList()) { + updateSiteList: for (UpdateSite updateSite : Jenkins.get().getUpdateCenter().getSiteList()) { String updateCenterJsonUrl = updateSite.getUrl(); String suggestedPluginUrl = updateCenterJsonUrl.replace("/update-center.json", "/platform-plugins.json"); try { @@ -404,7 +488,7 @@ public class SetupWizard extends PageDecorator { * Get the platform plugins added in the version range */ /*package*/ JSONArray getPlatformPluginsForUpdate(VersionNumber from, VersionNumber to) { - Jenkins jenkins = Jenkins.getInstance(); + Jenkins jenkins = Jenkins.get(); JSONArray pluginCategories = JSONArray.fromObject(getPlatformPluginList().toString()); for (Iterator categoryIterator = pluginCategories.iterator(); categoryIterator.hasNext();) { Object category = categoryIterator.next(); @@ -431,7 +515,7 @@ public class SetupWizard extends PageDecorator { for (UpdateSite site : jenkins.getUpdateCenter().getSiteList()) { UpdateSite.Plugin sitePlug = site.getPlugin(pluginName); if (sitePlug != null - && !sitePlug.isForNewerHudson() + && !sitePlug.isForNewerHudson() && !sitePlug.isForNewerJava() && !sitePlug.isNeededDependenciesForNewerJenkins()) { foundCompatibleVersion = true; break; @@ -461,7 +545,7 @@ public class SetupWizard extends PageDecorator { * Gets the file used to store the initial admin password */ public FilePath getInitialAdminPasswordFile() { - return Jenkins.getInstance().getRootPath().child("secrets/initialAdminPassword"); + return Jenkins.get().getRootPath().child("secrets/initialAdminPassword"); } /** @@ -474,11 +558,9 @@ public class SetupWizard extends PageDecorator { } /*package*/ void completeSetup() throws IOException, ServletException { - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.get().checkPermission(Jenkins.ADMINISTER); InstallUtil.saveLastExecVersion(); setCurrentLevel(Jenkins.getVersion()); - PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER); - isUsingSecurityToken = false; // this should not be considered new anymore InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_SETUP_COMPLETED); } @@ -498,7 +580,28 @@ public class SetupWizard extends PageDecorator { } return InstallState.valueOf(name); } - + + /** + * Called upon install state update. + * @param state the new install state. + * @since 2.94 + */ + public void onInstallStateUpdate(InstallState state) { + if (state.isSetupComplete()) { + tearDownFilter(); + } else { + setUpFilter(); + } + } + + /** + * Returns whether the setup wizard filter is currently registered. + * @since 2.94 + */ + public boolean hasSetupWizardFilter() { + return PluginServletFilter.hasFilter(FORCE_SETUP_WIZARD_FILTER); + } + /** * This filter will validate that the security token is provided */ @@ -517,7 +620,7 @@ public class SetupWizard extends PageDecorator { ((HttpServletResponse) response).sendRedirect(req.getContextPath() + "/"); return; } else if (req.getRequestURI().equals(req.getContextPath() + "/")) { - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.get().checkPermission(Jenkins.ADMINISTER); chain.doFilter(new HttpServletRequestWrapper(req) { public String getRequestURI() { return getContextPath() + "/setupWizard/"; @@ -534,4 +637,13 @@ public class SetupWizard extends PageDecorator { public void destroy() { } }; + + /** + * Sets up the Setup Wizard filter if the current state requires it. + */ + private void checkFilter() { + if (!Jenkins.get().getInstallState().isSetupComplete()) { + setUpFilter(); + } + } } diff --git a/core/src/main/java/jenkins/install/UpgradeWizard.java b/core/src/main/java/jenkins/install/UpgradeWizard.java index ab7e243a7039e125d757e5e2748d38e456bd13dd..57aef99ccd4e9ce4ce4447bc49702b0605f366f0 100644 --- a/core/src/main/java/jenkins/install/UpgradeWizard.java +++ b/core/src/main/java/jenkins/install/UpgradeWizard.java @@ -11,6 +11,7 @@ import java.util.logging.Logger; import javax.inject.Provider; import javax.servlet.http.HttpSession; +import jenkins.security.apitoken.ApiTokenPropertyConfiguration; import org.apache.commons.io.FileUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -56,6 +57,8 @@ public class UpgradeWizard extends InstallState { @Override public void initializeState() { + applyForcedChanges(); + // Initializing this state is directly related to // running the detached plugin checks, these should be consolidated somehow updateUpToDate(); @@ -68,6 +71,21 @@ public class UpgradeWizard extends InstallState { } } + /** + * Put here the different changes that are enforced after an update. + */ + private void applyForcedChanges(){ + // Disable the legacy system of API Token only if the new system was not installed + // in such case it means there was already an upgrade before + // and potentially the admin has re-enabled the features + ApiTokenPropertyConfiguration apiTokenPropertyConfiguration = ApiTokenPropertyConfiguration.get(); + if(!apiTokenPropertyConfiguration.hasExistingConfigFile()){ + LOGGER.log(Level.INFO, "New API token system configured with insecure options to keep legacy behavior"); + apiTokenPropertyConfiguration.setCreationOfLegacyTokenEnabled(false); + apiTokenPropertyConfiguration.setTokenGenerationOnCreationEnabled(false); + } + } + @Override public boolean isSetupComplete() { return !isDue(); diff --git a/core/src/main/java/jenkins/model/ArtifactManager.java b/core/src/main/java/jenkins/model/ArtifactManager.java index a7b569417ea82f7babd6f6ab485fc2af057950c5..874d4e3a5841d11abd436d31819b342d40837cb6 100644 --- a/core/src/main/java/jenkins/model/ArtifactManager.java +++ b/core/src/main/java/jenkins/model/ArtifactManager.java @@ -33,6 +33,7 @@ import hudson.model.TaskListener; import hudson.tasks.ArtifactArchiver; import java.io.IOException; import java.util.Map; +import javax.annotation.Nonnull; import jenkins.util.VirtualFile; /** @@ -47,7 +48,7 @@ public abstract class ArtifactManager { * The selected manager will be persisted inside a build, so the build reference should be {@code transient} (quasi-{@code final}) and restored here. * @param build a historical build with which this manager was associated */ - public abstract void onLoad(Run build); + public abstract void onLoad(@Nonnull Run build); /** * Archive all configured artifacts from a build. diff --git a/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java b/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java index 43f98aae8dbbb53afd121a5046f67391594e0a13..cb411800d954c984293e44cf6dcef4b467c7d44d 100644 --- a/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java +++ b/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java @@ -25,29 +25,28 @@ package jenkins.model; import hudson.Extension; +import hudson.model.PersistentDescriptor; import hudson.util.DescribableList; import java.io.IOException; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; + /** * List of configured {@link ArtifactManagerFactory}s. * @since 1.532 */ @Extension @Symbol("artifactManager") -public class ArtifactManagerConfiguration extends GlobalConfiguration { +public class ArtifactManagerConfiguration extends GlobalConfiguration implements PersistentDescriptor { - public static ArtifactManagerConfiguration get() { - return Jenkins.getInstance().getInjector().getInstance(ArtifactManagerConfiguration.class); + public static @Nonnull ArtifactManagerConfiguration get() { + return GlobalConfiguration.all().getInstance(ArtifactManagerConfiguration.class); } private final DescribableList artifactManagerFactories = new DescribableList(this); - public ArtifactManagerConfiguration() { - load(); - } - private Object readResolve() { artifactManagerFactories.setOwner(this); return this; diff --git a/core/src/main/java/jenkins/model/AssetManager.java b/core/src/main/java/jenkins/model/AssetManager.java index f6308e631da23859109416b4d40575d72cc664e0..ed148aac50b52b96a6e84de859ee24bddc9e4d77 100644 --- a/core/src/main/java/jenkins/model/AssetManager.java +++ b/core/src/main/java/jenkins/model/AssetManager.java @@ -2,7 +2,7 @@ package jenkins.model; import hudson.Extension; import hudson.model.UnprotectedRootAction; -import hudson.util.TimeUnit2; +import java.util.concurrent.TimeUnit; import org.jenkinsci.Symbol; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -60,7 +60,7 @@ public class AssetManager implements UnprotectedRootAction { // 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; + long expires = staticLink ? TimeUnit.DAYS.toMillis(365) : -1; // use serveLocalizedFile to support automatic locale selection rsp.serveLocalizedFile(req, resource, expires); diff --git a/core/src/main/java/jenkins/model/CauseOfInterruption.java b/core/src/main/java/jenkins/model/CauseOfInterruption.java index 7a8c173f212198258c2327347490a1c2a8229636..b14a02ca2752b0b6b3d904ea3123e933c742045a 100644 --- a/core/src/main/java/jenkins/model/CauseOfInterruption.java +++ b/core/src/main/java/jenkins/model/CauseOfInterruption.java @@ -39,7 +39,7 @@ import javax.annotation.Nonnull; * Records why an {@linkplain Executor#interrupt() executor is interrupted}. * *

    View

    - * summary.groovy/.jelly should do one-line HTML rendering to be used while rendering + * {@code summary.groovy/.jelly} should do one-line HTML rendering to be used while rendering * "build history" widget, next to the blocking build. By default it simply renders * {@link #getShortDescription()} text. * diff --git a/core/src/main/java/jenkins/model/DefaultSimplePageDecorator.java b/core/src/main/java/jenkins/model/DefaultSimplePageDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..6a4a9278a389132cc05bb85cca80893965d8db51 --- /dev/null +++ b/core/src/main/java/jenkins/model/DefaultSimplePageDecorator.java @@ -0,0 +1,36 @@ +/* + * The MIT License + * + * Copyright 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.model; + +import hudson.Extension; + +/** + * In case there are no other implementations we will fallback to this implementation. + * + * To make sure that we load this extension last (or at least very late) we use a negative ordinal. + * This allows custom implementation to be "first" + */ +@Extension(ordinal=-9999) +public class DefaultSimplePageDecorator extends SimplePageDecorator { +} diff --git a/core/src/main/java/jenkins/model/DownloadSettings.java b/core/src/main/java/jenkins/model/DownloadSettings.java index e4c78c2ef3fe1cc54270b6b55caa895733745052..6f5e6aeabbad6a753a09de3a11483c3b9b48f545 100644 --- a/core/src/main/java/jenkins/model/DownloadSettings.java +++ b/core/src/main/java/jenkins/model/DownloadSettings.java @@ -30,6 +30,7 @@ import hudson.model.AdministrativeMonitor; import hudson.model.AsyncPeriodicWork; import hudson.model.DownloadService; import hudson.model.DownloadService.Downloadable; +import hudson.model.PersistentDescriptor; import hudson.model.TaskListener; import hudson.model.UpdateSite; import hudson.util.FormValidation; @@ -42,6 +43,8 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; +import javax.annotation.Nonnull; + /** * Lets user configure how metadata files should be downloaded. * @see UpdateSite @@ -49,18 +52,14 @@ import org.kohsuke.stapler.HttpResponse; */ @Restricted(NoExternalUse.class) // no clear reason for this to be an API @Extension @Symbol("downloadSettings") -public final class DownloadSettings extends GlobalConfiguration { +public final class DownloadSettings extends GlobalConfiguration implements PersistentDescriptor { - public static DownloadSettings get() { - return Jenkins.getInstance().getInjector().getInstance(DownloadSettings.class); + public static @Nonnull DownloadSettings get() { + return GlobalConfiguration.all().getInstance(DownloadSettings.class); } private boolean useBrowser = false; - public DownloadSettings() { - load(); - } - public boolean isUseBrowser() { return useBrowser; } @@ -70,19 +69,19 @@ public final class DownloadSettings extends GlobalConfiguration { save(); } - @Override public GlobalConfigurationCategory getCategory() { + @Override public @Nonnull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); } public static boolean usePostBack() { - return get().isUseBrowser() && Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER); + return get().isUseBrowser() && Jenkins.get().hasPermission(Jenkins.ADMINISTER); } public static void checkPostBackAccess() throws AccessDeniedException { if (!get().isUseBrowser()) { throw new AccessDeniedException("browser-based download disabled"); } - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.get().checkPermission(Jenkins.ADMINISTER); } @Extension @Symbol("updateCenterCheck") @@ -106,7 +105,7 @@ public final class DownloadSettings extends GlobalConfiguration { return; } boolean due = false; - for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) { + for (UpdateSite site : Jenkins.get().getUpdateCenter().getSites()) { if (site.isDue()) { due = true; break; @@ -128,7 +127,7 @@ public final class DownloadSettings extends GlobalConfiguration { return; } // This checks updates of the update sites and downloadables. - HttpResponse rsp = Jenkins.getInstance().getPluginManager().doCheckUpdatesServer(); + HttpResponse rsp = Jenkins.get().getPluginManager().doCheckUpdatesServer(); if (rsp instanceof FormValidation) { listener.error(((FormValidation) rsp).renderHtml()); } diff --git a/core/src/main/java/jenkins/model/GlobalCloudConfiguration.java b/core/src/main/java/jenkins/model/GlobalCloudConfiguration.java index a4aa432e20765e28010cbed48405bf95dbef72f2..9c6635ab05e1a4ae09898adaf722e5d6b1a58779 100644 --- a/core/src/main/java/jenkins/model/GlobalCloudConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalCloudConfiguration.java @@ -21,7 +21,7 @@ public class GlobalCloudConfiguration extends GlobalConfiguration { @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { try { - Jenkins.getInstance().clouds.rebuildHetero(req,json, Cloud.all(), "cloud"); + Jenkins.get().clouds.rebuildHetero(req,json, Cloud.all(), "cloud"); return true; } catch (IOException e) { throw new FormException(e,"clouds"); diff --git a/core/src/main/java/jenkins/model/GlobalConfiguration.java b/core/src/main/java/jenkins/model/GlobalConfiguration.java index f751404bed11274f520ea1037a00775b0d763f1c..4a261cbed5d81eecdebee8bfa66ab600fdbde625 100644 --- a/core/src/main/java/jenkins/model/GlobalConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalConfiguration.java @@ -7,6 +7,8 @@ import hudson.model.Descriptor; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; +import javax.annotation.Nonnull; + /** * Convenient base class for extensions that contributes to the system configuration page but nothing * else, or to manage the global configuration of a plugin implementing several extension points. @@ -23,7 +25,7 @@ import org.kohsuke.stapler.StaplerRequest; * properties defined in your GlobalConfiguration subclass, here are two possibilities: *
    • @{@link javax.inject.Inject} into your other {@link hudson.Extension}s (so this does not work * for classes not annotated with {@link hudson.Extension})
    • - *
    • access it via a call to {@code GlobalConfiguration.all().get(.class)}
    + *
  • access it via a call to {@code ExtensionList.lookupSingleton(.class)}
* *

* While an implementation might store its actual configuration data in various ways, @@ -69,8 +71,8 @@ public abstract class GlobalConfiguration extends Descriptor all() { - return Jenkins.getInstance().getDescriptorList(GlobalConfiguration.class); + public static @Nonnull ExtensionList all() { + return Jenkins.get().getDescriptorList(GlobalConfiguration.class); // pointless type parameters help work around bugs in javac in earlier versions http://codepad.org/m1bbFRrH } } diff --git a/core/src/main/java/jenkins/model/GlobalConfigurationCategory.java b/core/src/main/java/jenkins/model/GlobalConfigurationCategory.java index d46329a404882bfff7d5505cb7509fe51a60c97b..5164f171e468741212c2da86358a350ef203b011 100644 --- a/core/src/main/java/jenkins/model/GlobalConfigurationCategory.java +++ b/core/src/main/java/jenkins/model/GlobalConfigurationCategory.java @@ -4,10 +4,10 @@ import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.model.ModelObject; -import hudson.security.*; -import hudson.security.Messages; import org.jenkinsci.Symbol; +import javax.annotation.Nonnull; + /** * Grouping of related {@link GlobalConfiguration}s. * @@ -41,8 +41,12 @@ public abstract class GlobalConfigurationCategory implements ExtensionPoint, Mod return ExtensionList.lookup(GlobalConfigurationCategory.class); } - public static T get(Class type) { - return all().get(type); + public static @Nonnull T get(Class type) { + T category = all().get(type); + if(category == null){ + throw new AssertionError("Category not found. It seems the " + type + " is not annotated with @Extension and so not registered"); + } + return category; } /** diff --git a/core/src/main/java/jenkins/model/GlobalNodePropertiesConfiguration.java b/core/src/main/java/jenkins/model/GlobalNodePropertiesConfiguration.java index 68bfc2bf0127c96a330964f06e5f988fb3e35635..b4cd78f46a70e5b5771c105ea5ee4b6e0b760505 100644 --- a/core/src/main/java/jenkins/model/GlobalNodePropertiesConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalNodePropertiesConfiguration.java @@ -19,7 +19,7 @@ public class GlobalNodePropertiesConfiguration extends GlobalConfiguration { @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { try { - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); JSONObject np = json.getJSONObject("globalNodeProperties"); if (!np.isNullObject()) { j.getGlobalNodeProperties().rebuild(req, np, NodeProperty.for_(j)); diff --git a/core/src/main/java/jenkins/model/GlobalPluginConfiguration.java b/core/src/main/java/jenkins/model/GlobalPluginConfiguration.java index a706573c0782ef2fee4c43899b1218abb93719a4..0454001f2a75f957d9ec24d26b42bf5cc62862e8 100644 --- a/core/src/main/java/jenkins/model/GlobalPluginConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalPluginConfiguration.java @@ -24,7 +24,7 @@ public class GlobalPluginConfiguration extends GlobalConfiguration { public boolean configure(StaplerRequest req, JSONObject json) throws FormException { try { for( JSONObject o : StructuredForm.toList(json, "plugin")) - Jenkins.getInstance().pluginManager.getPlugin(o.getString("name")).getPlugin().configure(req, o); + Jenkins.get().pluginManager.getPlugin(o.getString("name")).getPlugin().configure(req, o); return true; } catch (IOException | ServletException e) { throw new FormException(e,"plugin"); diff --git a/core/src/main/java/jenkins/model/GlobalProjectNamingStrategyConfiguration.java b/core/src/main/java/jenkins/model/GlobalProjectNamingStrategyConfiguration.java index ca07798af16ff9462546b704a6a7936f0da6e2ee..2502df1b6e1a98b99945ea20646176cd6666e488 100644 --- a/core/src/main/java/jenkins/model/GlobalProjectNamingStrategyConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalProjectNamingStrategyConfiguration.java @@ -41,13 +41,13 @@ public class GlobalProjectNamingStrategyConfiguration extends GlobalConfiguratio @Override public boolean configure(StaplerRequest req, JSONObject json) throws hudson.model.Descriptor.FormException { // for compatibility reasons, the actual value is stored in Jenkins - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); final JSONObject optJSONObject = json.optJSONObject("useProjectNamingStrategy"); if (optJSONObject != null) { final JSONObject strategyObject = optJSONObject.getJSONObject("namingStrategy"); final String className = strategyObject.getString("$class"); try { - Class clazz = Class.forName(className, true, Jenkins.getInstance().getPluginManager().uberClassLoader); + Class clazz = Class.forName(className, true, j.getPluginManager().uberClassLoader); final ProjectNamingStrategy strategy = (ProjectNamingStrategy) req.bindJSON(clazz, strategyObject); j.setProjectNamingStrategy(strategy); } catch (ClassNotFoundException e) { diff --git a/core/src/main/java/jenkins/model/GlobalQuietPeriodConfiguration.java b/core/src/main/java/jenkins/model/GlobalQuietPeriodConfiguration.java index cf1001793ac53d3d4e4fe4fc036980388852ae29..b0cdd84d3d3dd9916eaa89713ced86300312e90c 100644 --- a/core/src/main/java/jenkins/model/GlobalQuietPeriodConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalQuietPeriodConfiguration.java @@ -38,7 +38,7 @@ import java.io.IOException; @Extension(ordinal=400) @Symbol("quietPeriod") public class GlobalQuietPeriodConfiguration extends GlobalConfiguration { public int getQuietPeriod() { - return Jenkins.getInstance().getQuietPeriod(); + return Jenkins.get().getQuietPeriod(); } @Override @@ -51,7 +51,7 @@ public class GlobalQuietPeriodConfiguration extends GlobalConfiguration { } try { // for compatibility reasons, this value is stored in Jenkins - Jenkins.getInstance().setQuietPeriod(i); + Jenkins.get().setQuietPeriod(i); return true; } catch (IOException e) { throw new FormException(e,"quietPeriod"); diff --git a/core/src/main/java/jenkins/model/GlobalSCMRetryCountConfiguration.java b/core/src/main/java/jenkins/model/GlobalSCMRetryCountConfiguration.java index 3e4d3afafa3a28d92c56f776755d7e2bbdbb8698..32d0c155c600f7bf3129c2e15be282bdfc7908a4 100644 --- a/core/src/main/java/jenkins/model/GlobalSCMRetryCountConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalSCMRetryCountConfiguration.java @@ -39,14 +39,14 @@ import java.io.IOException; @Extension(ordinal=395) @Symbol("scmRetryCount") public class GlobalSCMRetryCountConfiguration extends GlobalConfiguration { public int getScmCheckoutRetryCount() { - return Jenkins.getInstance().getScmCheckoutRetryCount(); + return Jenkins.get().getScmCheckoutRetryCount(); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { try { // for compatibility reasons, this value is stored in Jenkins - Jenkins.getInstance().setScmCheckoutRetryCount(json.getInt("scmCheckoutRetryCount")); + Jenkins.get().setScmCheckoutRetryCount(json.getInt("scmCheckoutRetryCount")); return true; } catch (IOException e) { throw new FormException(e,"quietPeriod"); diff --git a/core/src/main/java/jenkins/model/IdStrategy.java b/core/src/main/java/jenkins/model/IdStrategy.java index 44b406c8423b6d71836b947ee0998a237b7c1778..7b216a143f7aa31da0395b33a8af79e6778a4bbb 100644 --- a/core/src/main/java/jenkins/model/IdStrategy.java +++ b/core/src/main/java/jenkins/model/IdStrategy.java @@ -30,11 +30,16 @@ import hudson.model.AbstractDescribableImpl; import hudson.util.CaseInsensitiveComparator; import org.apache.commons.lang.StringUtils; import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.ProtectedExternally; import org.kohsuke.stapler.DataBoundConstructor; import javax.annotation.Nonnull; import java.util.Comparator; import java.util.Locale; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * The strategy to use for manipulating converting names (e.g. user names, group names, etc) into ids. @@ -44,38 +49,63 @@ import java.util.Locale; public abstract class IdStrategy extends AbstractDescribableImpl implements ExtensionPoint, Comparator { + private static final Pattern PSEUDO_UNICODE_PATTERN = Pattern.compile("\\$[a-f0-9]{4}"); + private static final Pattern CAPITALIZATION_PATTERN = Pattern.compile("~[a-z]"); + /** * The default case insensitive strategy. */ public static IdStrategy CASE_INSENSITIVE = new CaseInsensitive(); /** - * Converts an ID into a name that for use as a filename. + * No longer used. This method is now a no-op but the signature is retained for backward compatibility. * - * @param id the id. Note, this method assumes that the id does not contain any filesystem unsafe characters. - * @return the name. + * @param id the id. + * @return the name. Must be filesystem safe. + * @deprecated No current use. */ - @Nonnull - public abstract String filenameOf(@Nonnull String id); + @Deprecated + public String filenameOf(@Nonnull String id) { + return null; + } /** - * Converts a filename into the corresponding id. + * No longer used. This method is now a no-op but the signature is retained for backward compatibility. + * + * @param id the id + * @return the name + * @deprecated No current use. + */ + @Deprecated + @Restricted(ProtectedExternally.class) + public String legacyFilenameOf(@Nonnull String id) { + return null; + } + + /** + * Converts a filename into the corresponding id. This may contain filesystem unsafe characters. + * * @param filename the filename. * @return the corresponding id. * @since 1.577 + * @deprecated Use only for migrating to new format. After the migration an id is no longer represented by a filename (directory). */ + @Deprecated public String idFromFilename(@Nonnull String filename) { return filename; } /** - * Converts an ID into a key for use in a Java Map. + * Converts an ID into a key for use in a Java Map or similar. This controls uniqueness of ids and how multiple different + * ids may map to the same id. For example, all different capitalizations of "Foo" may map to the same value "foo". * * @param id the id. * @return the key. */ @Nonnull - public abstract String keyFor(@Nonnull String id); + public String keyFor(@Nonnull String id) { + return id; + } /** * Compare two IDs and return {@code true} IFF the two ids are the same. Normally we expect that this should be @@ -91,7 +121,7 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp } /** - * Compare tow IDs and return their sorting order. If {@link #equals(String, String)} is {@code true} then this + * Compare two IDs and return their sorting order. If {@link #equals(String, String)} is {@code true} then this * must return {@code 0} but {@link #compare(String, String)} returning {@code 0} need not imply that * {@link #equals(String, String)} is {@code true}. * @@ -142,7 +172,26 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp * Returns all the registered {@link IdStrategy} descriptors. */ public static DescriptorExtensionList all() { - return Jenkins.getInstance().getDescriptorList(IdStrategy.class); + return Jenkins.get().getDescriptorList(IdStrategy.class); + } + + String applyPatternRepeatedly(@Nonnull Pattern pattern, @Nonnull String filename, + @Nonnull Function converter) { + StringBuilder id = new StringBuilder(); + int beginIndex = 0; + Matcher matcher = pattern.matcher(filename); + while (matcher.find()) { + String group = matcher.group(); + id.append(filename, beginIndex, matcher.start()); + id.append(converter.apply(group)); + beginIndex = matcher.end(); + } + id.append(filename.substring(beginIndex)); + return id.toString(); + } + + Character convertPseudoUnicode(String matchedGroup) { + return (char) Integer.parseInt(matchedGroup.substring(1), 16); } /** @@ -154,8 +203,8 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp public CaseInsensitive() {} @Override - @Nonnull - public String filenameOf(@Nonnull String id) { + public String idFromFilename(@Nonnull String filename) { + String id = applyPatternRepeatedly(PSEUDO_UNICODE_PATTERN, filename, this::convertPseudoUnicode); return id.toLowerCase(Locale.ENGLISH); } @@ -182,6 +231,7 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp /** * {@inheritDoc} */ + @Nonnull @Override public String getDisplayName() { return Messages.IdStrategy_CaseInsensitive_DisplayName(); @@ -197,86 +247,14 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp @DataBoundConstructor public CaseSensitive() {} - /** - * {@inheritDoc} - */ @Override - @Nonnull - public String filenameOf(@Nonnull String id) { - if (id.matches("[a-z0-9_. -]+")) { - return id; - } else { - StringBuilder buf = new StringBuilder(id.length() + 16); - for (char c : id.toCharArray()) { - if ('a' <= c && c <= 'z') { - buf.append(c); - } else if ('0' <= c && c <= '9') { - buf.append(c); - } else if ('_' == c || '.' == c || '-' == c || ' ' == c || '@' == c) { - buf.append(c); - } else if ('A' <= c && c <= 'Z') { - buf.append('~'); - buf.append(Character.toLowerCase(c)); - } else { - buf.append('$'); - buf.append(StringUtils.leftPad(Integer.toHexString(c & 0xffff), 4, '0')); - } - } - return buf.toString(); - } + public String idFromFilename(@Nonnull String filename) { + String id = applyPatternRepeatedly(CAPITALIZATION_PATTERN, filename, this::convertCapitalizedAscii); + return applyPatternRepeatedly(PSEUDO_UNICODE_PATTERN, id, this::convertPseudoUnicode); } - @Override - public String idFromFilename(@Nonnull String filename) { - if (filename.matches("[a-z0-9_. -]+")) { - return filename; - } else { - StringBuilder buf = new StringBuilder(filename.length()); - final char[] chars = filename.toCharArray(); - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - if ('a' <= c && c <= 'z') { - buf.append(c); - } else if ('0' <= c && c <= '9') { - buf.append(c); - } else if ('_' == c || '.' == c || '-' == c || ' ' == c || '@' == c) { - buf.append(c); - } else if (c == '~') { - i++; - if (i < chars.length) { - buf.append(Character.toUpperCase(chars[i])); - } - } else if (c == '$') { - StringBuilder hex = new StringBuilder(4); - i++; - if (i < chars.length) { - hex.append(chars[i]); - } else { - break; - } - i++; - if (i < chars.length) { - hex.append(chars[i]); - } else { - break; - } - i++; - if (i < chars.length) { - hex.append(chars[i]); - } else { - break; - } - i++; - if (i < chars.length) { - hex.append(chars[i]); - } else { - break; - } - buf.append(Character.valueOf((char)Integer.parseInt(hex.toString(), 16))); - } - } - return buf.toString(); - } + private Character convertCapitalizedAscii(String encoded) { + return encoded.toUpperCase().charAt(1); } /** @@ -287,15 +265,6 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp return StringUtils.equals(id1, id2); } - /** - * {@inheritDoc} - */ - @Override - @Nonnull - public String keyFor(@Nonnull String id) { - return id; - } - /** * {@inheritDoc} */ @@ -331,15 +300,6 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp @DataBoundConstructor public CaseSensitiveEmailAddress() {} - /** - * {@inheritDoc} - */ - @Override - @Nonnull - public String filenameOf(@Nonnull String id) { - return super.filenameOf(keyFor(id)); - } - /** * {@inheritDoc} */ diff --git a/core/src/main/java/jenkins/model/InvalidBuildsDir.java b/core/src/main/java/jenkins/model/InvalidBuildsDir.java new file mode 100644 index 0000000000000000000000000000000000000000..7e9b8f348dc7a56935058a3c50900dfa82f46c7f --- /dev/null +++ b/core/src/main/java/jenkins/model/InvalidBuildsDir.java @@ -0,0 +1,16 @@ +package jenkins.model; + +import hudson.util.BootFailure; + +public class InvalidBuildsDir extends BootFailure { + private String message; + + public InvalidBuildsDir(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 34dce65e41bf90631c974a27c70da2f714c86f0d..fee5e77f15adfd217c408f46ff877274ab598c94 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -27,6 +27,7 @@ package jenkins.model; import antlr.ANTLRException; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -37,6 +38,11 @@ import hudson.*; import hudson.Launcher.LocalLauncher; import jenkins.AgentProtocol; import jenkins.diagnostics.URICheckEncodingMonitor; +import jenkins.security.stapler.DoActionFilter; +import jenkins.security.stapler.StaplerFilteredActionListener; +import jenkins.security.stapler.StaplerDispatchable; +import jenkins.security.RedactSecretJsonInErrorMessageSanitizer; +import jenkins.security.stapler.TypedFilter; import jenkins.util.SystemProperties; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; @@ -106,7 +112,6 @@ import hudson.model.listeners.ItemListener; import hudson.model.listeners.SCMListener; import hudson.model.listeners.SaveableListener; import hudson.remoting.Callable; -import hudson.remoting.ClassFilter; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; import hudson.scm.RepositoryBrowser; @@ -158,14 +163,12 @@ import hudson.util.HudsonIsLoading; import hudson.util.HudsonIsRestarting; import hudson.util.Iterators; import hudson.util.JenkinsReloadFailed; -import hudson.util.Memoizer; import hudson.util.MultipartFormDataParser; import hudson.util.NamingThreadFactory; import hudson.util.PluginServletFilter; import hudson.util.RemotingDiagnostics; import hudson.util.RemotingDiagnostics.HeapDump; import hudson.util.TextFile; -import hudson.util.TimeUnit2; import hudson.util.VersionNumber; import hudson.util.XStream2; import hudson.views.DefaultMyViewsTabBar; @@ -181,9 +184,9 @@ import jenkins.ExtensionComponentSet; import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; import jenkins.install.InstallState; -import jenkins.install.InstallUtil; import jenkins.install.SetupWizard; import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy; +import jenkins.security.ClassFilterImpl; import jenkins.security.ConfidentialKey; import jenkins.security.ConfidentialStore; import jenkins.security.SecurityListener; @@ -251,7 +254,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; -import java.lang.reflect.Field; import java.net.BindException; import java.net.HttpURLConnection; import java.net.URL; @@ -287,7 +289,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; -import java.util.regex.Pattern; import java.util.stream.Collectors; import static hudson.Util.*; @@ -295,6 +296,7 @@ import static hudson.init.InitMilestone.*; import hudson.init.Initializer; import hudson.util.LogTaskListener; import static java.util.logging.Level.*; +import javax.annotation.Nonnegative; import static javax.servlet.http.HttpServletResponse.*; import org.kohsuke.stapler.WebMethod; @@ -331,7 +333,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * The Jenkins instance startup type i.e. NEW, UPGRADE etc */ - private transient InstallState installState = InstallState.UNKNOWN; + private String installStateName; + + @Deprecated + private InstallState installState; /** * If we're in the process of an initial setup, @@ -403,14 +408,14 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * This value will be variable-expanded as per {@link #expandVariablesForDirectory}. * @see #getWorkspaceFor(TopLevelItem) */ - private String workspaceDir = "${ITEM_ROOTDIR}/"+WORKSPACE_DIRNAME; + private String workspaceDir = OLD_DEFAULT_WORKSPACES_DIR; /** * Root directory for the builds. * This value will be variable-expanded as per {@link #expandVariablesForDirectory}. * @see #getBuildDirFor(Job) */ - private String buildsDir = "${ITEM_ROOTDIR}/builds"; + private String buildsDir = DEFAULT_BUILDS_DIR; /** * Message displayed in the top page. @@ -444,6 +449,17 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @GuardedBy("Jenkins.class") private transient boolean cleanUpStarted; + /** + * Use this to know during startup if this is a fresh one, aka first-time, startup, or a later one. + * A file will be created at the very end of the Jenkins initialization process. + * I.e. if the file is present, that means this is *NOT* a fresh startup. + * + * + * STARTUP_MARKER_FILE.get(); // returns false if we are on a fresh startup. True for next startups. + * + */ + private transient static FileBoolean STARTUP_MARKER_FILE; + private volatile List jdks = new ArrayList(); private transient volatile DependencyGraph dependencyGraph; @@ -462,20 +478,14 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * All {@link ExtensionList} keyed by their {@link ExtensionList#extensionType}. */ - private transient final Memoizer extensionLists = new Memoizer() { - public ExtensionList compute(Class key) { - return ExtensionList.create(Jenkins.this,key); - } - }; + @SuppressWarnings("rawtypes") + private transient final Map extensionLists = new ConcurrentHashMap<>(); /** * All {@link DescriptorExtensionList} keyed by their {@link DescriptorExtensionList#describableType}. */ - private transient final Memoizer descriptorLists = new Memoizer() { - public DescriptorExtensionList compute(Class key) { - return DescriptorExtensionList.createDescriptorList(Jenkins.this,key); - } - }; + @SuppressWarnings("rawtypes") + private transient final Map descriptorLists = new ConcurrentHashMap<>(); /** * {@link Computer}s in this Jenkins system. Read-only. @@ -748,61 +758,62 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * Gets the {@link Jenkins} singleton. - * {@link #getInstanceOrNull()} provides the unchecked versions of the method. * @return {@link Jenkins} instance - * @throws IllegalStateException {@link Jenkins} has not been started, or was already shut down - * @since 1.590 - * @deprecated use {@link #getInstance()} + * @throws IllegalStateException for the reasons that {@link #getInstanceOrNull} might return null + * @since 2.98 */ - @Deprecated @Nonnull - public static Jenkins getActiveInstance() throws IllegalStateException { - Jenkins instance = HOLDER.getInstance(); + public static Jenkins get() throws IllegalStateException { + Jenkins instance = getInstanceOrNull(); if (instance == null) { - throw new IllegalStateException("Jenkins has not been started, or was already shut down"); + throw new IllegalStateException("Jenkins.instance is missing. Read the documentation of Jenkins.getInstanceOrNull to see what you are doing wrong."); } return instance; } + /** + * @deprecated This is a verbose historical alias for {@link #get}. + * @since 1.590 + */ + @Deprecated + @Nonnull + public static Jenkins getActiveInstance() throws IllegalStateException { + return get(); + } + /** * Gets the {@link Jenkins} singleton. - * {@link #getActiveInstance()} provides the checked versions of the method. - * @return The instance. Null if the {@link Jenkins} instance has not been started, - * or was already shut down + * {@link #get} is what you normally want. + *

In certain rare cases you may have code that is intended to run before Jenkins starts or while Jenkins is being shut down. + * For those rare cases use this method. + *

In other cases you may have code that might end up running on a remote JVM and not on the Jenkins master. + * For those cases you really should rewrite your code so that when the {@link Callable} is sent over the remoting channel + * it can do whatever it needs without ever referring to {@link Jenkins}; + * for example, gather any information you need on the master side before constructing the callable. + * If you must do a runtime check whether you are in the master or agent, use {@link JenkinsJVM} rather than this method, + * as merely loading the {@link Jenkins} class file into an agent JVM can cause linkage errors under some conditions. + * @return The instance. Null if the {@link Jenkins} service has not been started, or was already shut down, + * or we are running on an unrelated JVM, typically an agent. * @since 1.653 */ + @CLIResolver @CheckForNull public static Jenkins getInstanceOrNull() { return HOLDER.getInstance(); } /** - * Gets the {@link Jenkins} singleton. In certain rare cases you may have code that is intended to run before - * Jenkins starts or while Jenkins is being shut-down. For those rare cases use {@link #getInstanceOrNull()}. - * In other cases you may have code that might end up running on a remote JVM and not on the Jenkins master, - * for those cases you really should rewrite your code so that when the {@link Callable} is sent over the remoting - * channel it uses a {@code writeReplace} method or similar to ensure that the {@link Jenkins} class is not being - * loaded into the remote class loader - * @return The instance. - * @throws IllegalStateException {@link Jenkins} has not been started, or was already shut down + * @deprecated This is a historical alias for {@link #getInstanceOrNull} but with ambiguous nullability. Use {@link #get} in typical cases. */ - @CLIResolver - @Nonnull + @Nullable + @Deprecated public static Jenkins getInstance() { - Jenkins instance = HOLDER.getInstance(); - if (instance == null) { - if(SystemProperties.getBoolean(Jenkins.class.getName()+".enableExceptionOnNullInstance")) { - // TODO: remove that second block around 2.20 (that is: ~20 versions to battle test it) - // See https://github.com/jenkinsci/jenkins/pull/2297#issuecomment-216710150 - throw new IllegalStateException("Jenkins has not been started, or was already shut down"); - } - } - return instance; + return getInstanceOrNull(); } /** * Secret key generated once and used for a long time, beyond - * container start/stop. Persisted outside config.xml to avoid + * container start/stop. Persisted outside {@code config.xml} to avoid * accidental exposure. */ private transient final String secretKey; @@ -842,7 +853,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve oldJenkinsJVM = JenkinsJVM.isJenkinsJVM(); // capture to restore in cleanUp() JenkinsJVMAccess._setJenkinsJVM(true); // set it for unit tests as they will not have gone through WebAppMain long start = System.currentTimeMillis(); - + STARTUP_MARKER_FILE = new FileBoolean(new File(root, ".lastStarted")); // As Jenkins is starting, grant this process full control ACL.impersonate(ACL.SYSTEM); try { @@ -855,7 +866,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (!new File(root,"jobs").exists()) { // if this is a fresh install, use more modern default layout that's consistent with agents - workspaceDir = "${JENKINS_HOME}/workspace/${ITEM_FULLNAME}"; + workspaceDir = DEFAULT_WORKSPACES_DIR; } // doing this early allows InitStrategy to set environment upfront @@ -898,32 +909,24 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (pluginManager==null) pluginManager = PluginManager.createDefault(this); this.pluginManager = pluginManager; + WebApp webApp = WebApp.get(servletContext); // JSON binding needs to be able to see all the classes from all the plugins - WebApp.get(servletContext).setClassLoader(pluginManager.uberClassLoader); + webApp.setClassLoader(pluginManager.uberClassLoader); + webApp.setJsonInErrorMessageSanitizer(RedactSecretJsonInErrorMessageSanitizer.INSTANCE); - adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit2.DAYS.toMillis(365)); + TypedFilter typedFilter = new TypedFilter(); + webApp.setFilterForGetMethods(typedFilter); + webApp.setFilterForFields(typedFilter); + webApp.setFilterForDoActions(new DoActionFilter()); - // TODO pending move to standard blacklist, or API to append filter - if (System.getProperty(ClassFilter.FILE_OVERRIDE_LOCATION_PROPERTY) == null) { // not using SystemProperties since ClassFilter does not either - try { - Field blacklistPatternsF = ClassFilter.DEFAULT.getClass().getDeclaredField("blacklistPatterns"); - blacklistPatternsF.setAccessible(true); - Object[] blacklistPatternsA = (Object[]) blacklistPatternsF.get(ClassFilter.DEFAULT); - boolean found = false; - for (int i = 0; i < blacklistPatternsA.length; i++) { - if (blacklistPatternsA[i] instanceof Pattern) { - blacklistPatternsA[i] = Pattern.compile("(" + blacklistPatternsA[i] + ")|(java[.]security[.]SignedObject)"); - found = true; - break; - } - } - if (!found) { - throw new Error("no Pattern found among " + Arrays.toString(blacklistPatternsA)); - } - } catch (NoSuchFieldException | IllegalAccessException x) { - throw new Error("Unexpected ClassFilter implementation in bundled remoting.jar: " + x, x); - } - } + StaplerFilteredActionListener actionListener = new StaplerFilteredActionListener(); + webApp.setFilteredGetterTriggerListener(actionListener); + webApp.setFilteredDoActionTriggerListener(actionListener); + webApp.setFilteredFieldTriggerListener(actionListener); + + adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit.DAYS.toMillis(365)); + + ClassFilterImpl.register(); // initialization consists of ... executeReactor( is, @@ -947,9 +950,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if(KILL_AFTER_LOAD) // TODO cleanUp? System.exit(0); - - setupWizard = new SetupWizard(); - InstallUtil.proceedToNextStateFrom(InstallState.UNKNOWN); + save(); launchTcpSlaveAgentListener(); @@ -968,7 +969,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve protected void doRun() throws Exception { trimLabels(); } - }, TimeUnit2.MINUTES.toMillis(5), TimeUnit2.MINUTES.toMillis(5), TimeUnit.MILLISECONDS); + }, TimeUnit.MINUTES.toMillis(5), TimeUnit.MINUTES.toMillis(5), TimeUnit.MILLISECONDS); updateComputerList(); @@ -1008,6 +1009,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (LOG_STARTUP_PERFORMANCE) LOGGER.info(String.format("Took %dms for complete Jenkins startup", System.currentTimeMillis()-start)); + + STARTUP_MARKER_FILE.on(); } finally { SecurityContextHolder.clearContext(); } @@ -1042,25 +1045,28 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * @return The Jenkins {@link jenkins.install.InstallState install state}. */ @Nonnull - @Restricted(NoExternalUse.class) public InstallState getInstallState() { - if (installState == null || installState.name() == null) { - return InstallState.UNKNOWN; + if (installState != null) { + installStateName = installState.name(); + installState = null; } - return installState; + InstallState is = installStateName != null ? InstallState.valueOf(installStateName) : InstallState.UNKNOWN; + return is != null ? is : InstallState.UNKNOWN; } /** * Update the current install state. This will invoke state.initializeState() * when the state has been transitioned. */ - @Restricted(NoExternalUse.class) public void setInstallState(@Nonnull InstallState newState) { - InstallState prior = installState; - installState = newState; - if (!prior.equals(newState)) { + String prior = installStateName; + installStateName = newState.name(); + LOGGER.log(Main.isDevelopmentMode ? Level.INFO : Level.FINE, "Install state transitioning from: {0} to : {1}", new Object[] { prior, installStateName }); + if (!installStateName.equals(prior)) { + getSetupWizard().onInstallStateUpdate(newState); newState.initializeState(); } + saveQuietly(); } /** @@ -1322,6 +1328,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return systemMessage; } + @Nonnull public PluginManager getPluginManager() { return pluginManager; } @@ -1532,6 +1539,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * If the descriptor is missing. * @since 1.326 */ + @Nonnull public Descriptor getDescriptorOrDie(Class type) { Descriptor d = getDescriptor(type); if (d==null) @@ -1581,7 +1589,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * Gets the plugin object from its short name. - * This allows URL hudson/plugin/ID to be served by the views + * This allows URL {@code hudson/plugin/ID} to be served by the views * of the plugin class. * @param shortName Short name of the plugin * @return The plugin singleton or {@code null} if for some reason the plugin is not loaded. @@ -1668,6 +1676,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve save(); } + @StaplerDispatchable public FederatedLoginService getFederatedLoginService(String name) { for (FederatedLoginService fls : FederatedLoginService.all()) { if (fls.getUrlName().equals(name)) @@ -1751,42 +1760,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return r; } - /** - * Gets all the {@link Item}s recursively in the {@link ItemGroup} tree - * and filter them by the given type. - */ - public List getAllItems(Class type) { - return Items.getAllItems(this, type); - } - - /** - * Gets all the {@link Item}s unordered, lazily and recursively in the {@link ItemGroup} tree - * and filter them by the given type. - * - * @since 2.37 - */ - public Iterable allItems(Class type) { - return Items.allItems(this, type); - } - - /** - * Gets all the items recursively. - * - * @since 1.402 - */ - public List getAllItems() { - return getAllItems(Item.class); - } - - /** - * Gets all the items unordered, lazily and recursively. - * - * @since 2.37 - */ - public Iterable allItems() { - return allItems(Item.class); - } - /** * Gets a list of simple top-level projects. * @deprecated This method will ignore Maven and matrix projects, as well as projects inside containers such as folders. @@ -2166,7 +2139,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * Binds {@link AdministrativeMonitor}s to URL. + * @param id Monitor ID + * @return The requested monitor or {@code null} if it does not exist */ + @CheckForNull public AdministrativeMonitor getAdministrativeMonitor(String id) { for (AdministrativeMonitor m : administrativeMonitors) if(m.id.equals(id)) @@ -2176,7 +2152,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * Returns the enabled and activated administrative monitors. - * @since TODO + * @since 2.64 */ public List getActiveAdministrativeMonitors() { return administrativeMonitors.stream().filter(m -> m.isEnabled() && m.isActivated()).collect(Collectors.toList()); @@ -2199,46 +2175,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return FormValidation.validateNonNegativeInteger(value); } - public FormValidation doCheckRawBuildsDir(@QueryParameter String value) { - // do essentially what expandVariablesForDirectory does, without an Item - String replacedValue = expandVariablesForDirectory(value, - "doCheckRawBuildsDir-Marker:foo", - Jenkins.getInstance().getRootDir().getPath() + "/jobs/doCheckRawBuildsDir-Marker$foo"); - - File replacedFile = new File(replacedValue); - if (!replacedFile.isAbsolute()) { - return FormValidation.error(value + " does not resolve to an absolute path"); - } - - if (!replacedValue.contains("doCheckRawBuildsDir-Marker")) { - return FormValidation.error(value + " does not contain ${ITEM_FULL_NAME} or ${ITEM_ROOTDIR}, cannot distinguish between projects"); - } - - if (replacedValue.contains("doCheckRawBuildsDir-Marker:foo")) { - // make sure platform can handle colon - try { - File tmp = File.createTempFile("Jenkins-doCheckRawBuildsDir", "foo:bar"); - tmp.delete(); - } catch (IOException e) { - return FormValidation.error(value + " contains ${ITEM_FULLNAME} but your system does not support it (JENKINS-12251). Use ${ITEM_FULL_NAME} instead"); - } - } - - File d = new File(replacedValue); - if (!d.isDirectory()) { - // if dir does not exist (almost guaranteed) need to make sure nearest existing ancestor can be written to - d = d.getParentFile(); - while (!d.exists()) { - d = d.getParentFile(); - } - if (!d.canWrite()) { - return FormValidation.error(value + " does not exist and probably cannot be created"); - } - } - - return FormValidation.ok(); - } - // to route /descriptor/FQCN/xxx to getDescriptor(FQCN).xxx public Object getDynamic(String token) { return Jenkins.getInstance().getDescriptor(token); @@ -2326,12 +2262,21 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * It is done in this order so that it can work correctly even in the face * of a reverse proxy. * - * @return null if this parameter is not configured by the user and the calling thread is not in an HTTP request; otherwise the returned URL will always have the trailing {@code /} + * @return {@code null} if this parameter is not configured by the user and the calling thread is not in an HTTP request; + * otherwise the returned URL will always have the trailing {@code /} + * @throws IllegalStateException {@link JenkinsLocationConfiguration} cannot be retrieved. + * Jenkins instance may be not ready, or there is an extension loading glitch. * @since 1.66 * @see Hyperlinks in HTML */ - public @Nullable String getRootUrl() { - String url = JenkinsLocationConfiguration.get().getUrl(); + public @Nullable String getRootUrl() throws IllegalStateException { + final JenkinsLocationConfiguration config = JenkinsLocationConfiguration.get(); + if (config == null) { + // Try to get standard message if possible + final Jenkins j = Jenkins.getInstance(); + throw new IllegalStateException("Jenkins instance " + j + " has been successfully initialized, but JenkinsLocationConfiguration is undefined."); + } + String url = config.getUrl(); if(url!=null) { return Util.ensureEndsWith(url,"/"); } @@ -2447,12 +2392,27 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return expandVariablesForDirectory(buildsDir, job); } + /** + * If the configured buildsDir has it's default value or has been changed. + * + * @return true if default value. + */ + @Restricted(NoExternalUse.class) + public boolean isDefaultBuildDir() { + return DEFAULT_BUILDS_DIR.equals(buildsDir); + } + + @Restricted(NoExternalUse.class) + boolean isDefaultWorkspaceDir() { + return OLD_DEFAULT_WORKSPACES_DIR.equals(workspaceDir) || DEFAULT_WORKSPACES_DIR.equals(workspaceDir); + } + private File expandVariablesForDirectory(String base, Item item) { return new File(expandVariablesForDirectory(base, item.getFullName(), item.getRootDir().getPath())); } @Restricted(NoExternalUse.class) - static String expandVariablesForDirectory(String base, String itemFullName, String itemRootDir) { + public static String expandVariablesForDirectory(String base, String itemFullName, String itemRootDir) { return Util.replaceMacro(base, ImmutableMap.of( "JENKINS_HOME", Jenkins.getInstance().getRootDir().getPath(), "ITEM_ROOTDIR", itemRootDir, @@ -2489,11 +2449,13 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @Override public Callable getClockDifferenceCallable() { - return new MasterToSlaveCallable() { - public ClockDifference call() throws IOException { - return new ClockDifference(0); - } - }; + return new ClockDifferenceCallable(); + } + private static class ClockDifferenceCallable extends MasterToSlaveCallable { + @Override + public ClockDifference call() throws IOException { + return new ClockDifference(0); + } } /** @@ -2618,7 +2580,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * * @since 1.433 */ - public Injector getInjector() { + public @CheckForNull Injector getInjector() { return lookup(Injector.class); } @@ -2634,7 +2596,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @SuppressWarnings({"unchecked"}) public ExtensionList getExtensionList(Class extensionType) { - return extensionLists.get(extensionType); + ExtensionList extensionList = extensionLists.get(extensionType); + return extensionList != null ? extensionList : extensionLists.computeIfAbsent(extensionType, key -> ExtensionList.create(this, key)); } /** @@ -2642,6 +2605,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * * @since 1.349 */ + @StaplerDispatchable public ExtensionList getExtensionList(String extensionType) throws ClassNotFoundException { return getExtensionList(pluginManager.uberClassLoader.loadClass(extensionType)); } @@ -2654,8 +2618,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * Can be an empty list but never null. */ @SuppressWarnings({"unchecked"}) - public ,D extends Descriptor> DescriptorExtensionList getDescriptorList(Class type) { - return descriptorLists.get(type); + public @Nonnull ,D extends Descriptor> DescriptorExtensionList getDescriptorList(Class type) { + return descriptorLists.computeIfAbsent(type, key -> DescriptorExtensionList.createDescriptorList(this, key)); } /** @@ -2756,7 +2720,16 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return initLevel; } - public void setNumExecutors(int n) throws IOException { + /** + * Sets a number of executors. + * @param n Number of executors + * @throws IOException Failed to save the configuration + * @throws IllegalArgumentException Negative value has been passed + */ + public void setNumExecutors(@Nonnegative int n) throws IOException, IllegalArgumentException { + if (n < 0) { + throw new IllegalArgumentException("Incorrect field \"# of executors\": " + n +". It should be a non-negative number."); + } if (this.numExecutors != n) { this.numExecutors = n; updateComputerList(); @@ -3002,6 +2975,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } // if no finger print matches, display "not found page". + @StaplerDispatchable public Object getFingerprint( String md5sum ) throws IOException { Fingerprint r = fingerprintMap.get(md5sum); if(r==null) return new NoFingerprintMatch(md5sum); @@ -3051,6 +3025,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return getLabelAtom("master"); } + @Override + @Nonnull public Computer createComputer() { return new Hudson.MasterComputer(); } @@ -3066,6 +3042,90 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve // load from disk cfg.unmarshal(Jenkins.this); } + + try { + checkRawBuildsDir(buildsDir); + setBuildsAndWorkspacesDir(); + } catch (InvalidBuildsDir invalidBuildsDir) { + throw new IOException(invalidBuildsDir); + } + + } + + private void setBuildsAndWorkspacesDir() throws IOException, InvalidBuildsDir { + boolean mustSave = false; + String newBuildsDir = SystemProperties.getString(BUILDS_DIR_PROP); + boolean freshStartup = STARTUP_MARKER_FILE.isOff(); + if (newBuildsDir != null && !buildsDir.equals(newBuildsDir)) { + + checkRawBuildsDir(newBuildsDir); + Level level = freshStartup ? Level.INFO : Level.WARNING; + LOGGER.log(level, "Changing builds directories from {0} to {1}. Beware that no automated data migration will occur.", + new String[]{buildsDir, newBuildsDir}); + buildsDir = newBuildsDir; + mustSave = true; + } else if (!isDefaultBuildDir()) { + LOGGER.log(Level.INFO, "Using non default builds directories: {0}.", buildsDir); + } + + String newWorkspacesDir = SystemProperties.getString(WORKSPACES_DIR_PROP); + if (newWorkspacesDir != null && !workspaceDir.equals(newWorkspacesDir)) { + Level level = freshStartup ? Level.INFO : Level.WARNING; + LOGGER.log(level, "Changing workspaces directories from {0} to {1}. Beware that no automated data migration will occur.", + new String[]{workspaceDir, newWorkspacesDir}); + workspaceDir = newWorkspacesDir; + mustSave = true; + } else if (!isDefaultWorkspaceDir()) { + LOGGER.log(Level.INFO, "Using non default workspaces directories: {0}.", workspaceDir); + } + + if (mustSave) { + save(); + } + } + + /** + * Checks the correctness of the newBuildsDirValue for use as {@link #buildsDir}. + * @param newBuildsDirValue the candidate newBuildsDirValue for updating {@link #buildsDir}. + */ + @VisibleForTesting + /*private*/ static void checkRawBuildsDir(String newBuildsDirValue) throws InvalidBuildsDir { + + // do essentially what expandVariablesForDirectory does, without an Item + String replacedValue = expandVariablesForDirectory(newBuildsDirValue, + "doCheckRawBuildsDir-Marker:foo", + Jenkins.getInstance().getRootDir().getPath() + "/jobs/doCheckRawBuildsDir-Marker$foo"); + + File replacedFile = new File(replacedValue); + if (!replacedFile.isAbsolute()) { + throw new InvalidBuildsDir(newBuildsDirValue + " does not resolve to an absolute path"); + } + + if (!replacedValue.contains("doCheckRawBuildsDir-Marker")) { + throw new InvalidBuildsDir(newBuildsDirValue + " does not contain ${ITEM_FULL_NAME} or ${ITEM_ROOTDIR}, cannot distinguish between projects"); + } + + if (replacedValue.contains("doCheckRawBuildsDir-Marker:foo")) { + // make sure platform can handle colon + try { + File tmp = File.createTempFile("Jenkins-doCheckRawBuildsDir", "foo:bar"); + tmp.delete(); + } catch (IOException e) { + throw new InvalidBuildsDir(newBuildsDirValue + " contains ${ITEM_FULLNAME} but your system does not support it (JENKINS-12251). Use ${ITEM_FULL_NAME} instead"); + } + } + + File d = new File(replacedValue); + if (!d.isDirectory()) { + // if dir does not exist (almost guaranteed) need to make sure nearest existing ancestor can be written to + d = d.getParentFile(); + while (!d.exists()) { + d = d.getParentFile(); + } + if (!d.canWrite()) { + throw new InvalidBuildsDir(newBuildsDirValue + " does not exist and probably cannot be created"); + } + } } private synchronized TaskBuilder loadTasks() throws IOException { @@ -3095,8 +3155,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } }); + List loadJobs = new ArrayList<>(); for (final File subdir : subdirs) { - g.requires(loadJenkins).attains(JOB_LOADED).notFatal().add("Loading item " + subdir.getName(), new Executable() { + loadJobs.add(g.requires(loadJenkins).attains(JOB_LOADED).notFatal().add("Loading item " + subdir.getName(), new Executable() { public void run(Reactor session) throws Exception { if(!Items.getConfigFile(subdir).exists()) { //Does not have job config file, so it is not a jenkins job hence skip it @@ -3106,10 +3167,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve items.put(item.getName(), item); loadedNames.add(item.getName()); } - }); + })); } - g.requires(JOB_LOADED).add("Cleaning up obsolete items deleted from the disk", new Executable() { + g.requires(loadJobs.toArray(new Handle[loadJobs.size()])).attains(JOB_LOADED).add("Cleaning up obsolete items deleted from the disk", new Executable() { public void run(Reactor reactor) throws Exception { // anything we didn't load from disk, throw them away. // doing this after loading from disk allows newly loaded items @@ -3124,7 +3185,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } }); - g.requires(JOB_LOADED).add("Finalizing set up",new Executable() { + g.requires(JOB_LOADED).attains(COMPLETED).add("Finalizing set up",new Executable() { public void run(Reactor session) throws Exception { rebuildDependencyGraph(); @@ -3176,6 +3237,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve // auto register root actions for (Action a : getExtensionList(RootAction.class)) if (!actions.contains(a)) actions.add(a); + + setupWizard = new SetupWizard(); + getInstallState().initializeState(); } }); @@ -3187,6 +3251,14 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ public synchronized void save() throws IOException { if(BulkChange.contains(this)) return; + + if (initLevel == InitMilestone.COMPLETED) { + LOGGER.log(FINE, "setting version {0} to {1}", new Object[] {version, VERSION}); + version = VERSION; + } else { + LOGGER.log(FINE, "refusing to set version {0} to {1} during {2}", new Object[] {version, VERSION, initLevel}); + } + getConfigFile().write(this); SaveableListener.fireOnChange(this, getConfigFile()); } @@ -3274,6 +3346,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (JenkinsJVM.isJenkinsJVM()) { JenkinsJVMAccess._setJenkinsJVM(oldJenkinsJVM); } + ClassFilterImpl.unregister(); } } @@ -3489,17 +3562,17 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve private void _cleanUpShutdownTcpSlaveAgent(List errors) { if(tcpSlaveAgentListener!=null) { - LOGGER.log(FINE, "Shutting down TCP/IP slave agent listener"); + LOGGER.log(FINE, "Shutting down TCP/IP agent listener"); try { tcpSlaveAgentListener.shutdown(); } catch (OutOfMemoryError e) { // we should just propagate this, no point trying to log throw e; } catch (LinkageError e) { - LOGGER.log(SEVERE, "Failed to shut down TCP/IP slave agent listener", e); + LOGGER.log(SEVERE, "Failed to shut down TCP/IP agent listener", e); // safe to ignore and continue for this one } catch (Throwable e) { - LOGGER.log(SEVERE, "Failed to shut down TCP/IP slave agent listener", e); + LOGGER.log(SEVERE, "Failed to shut down TCP/IP agent listener", e); // save for later errors.add(e); } @@ -3656,17 +3729,12 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve JSONObject json = req.getSubmittedForm(); - workspaceDir = json.getString("rawWorkspaceDir"); - buildsDir = json.getString("rawBuildsDir"); - systemMessage = Util.nullify(req.getParameter("system_message")); boolean result = true; for (Descriptor d : Functions.getSortedDescriptorsForGlobalConfigUnclassified()) result &= configureDescriptor(req,json,d); - - version = VERSION; - + save(); updateComputerList(); if(result) @@ -3715,9 +3783,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve try { JSONObject json = req.getSubmittedForm(); - MasterBuildConfiguration mbc = MasterBuildConfiguration.all().get(MasterBuildConfiguration.class); - if (mbc!=null) - mbc.configure(req,json); + ExtensionList.lookupSingleton(MasterBuildConfiguration.class).configure(req,json); getNodeProperties().rebuild(req, json.optJSONObject("nodeProperties"), NodeProperty.all()); } finally { @@ -3986,7 +4052,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve LOGGER.log(Level.WARNING, "Reloading Jenkins as requested by {0}", getAuthentication().getName()); // engage "loading ..." UI and then run the actual task in a separate thread - servletContext.setAttribute("app", new HudsonIsLoading()); + WebApp.get(servletContext).setApp(new HudsonIsLoading()); new Thread("Jenkins config reload thread") { @Override @@ -4025,7 +4091,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve User.reload(); queue.load(); - servletContext.setAttribute("app", this); + WebApp.get(servletContext).setApp(this); } /** @@ -4061,6 +4127,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * End point that intentionally throws an exception to test the error behaviour. * @since 1.467 */ + @StaplerDispatchable public void doException() { throw new RuntimeException(); } @@ -4154,12 +4221,20 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve return HttpResponses.redirectToDot(); } + private static Lifecycle restartableLifecycle() throws RestartNotSupportedException { + if (Main.isUnitTest) { + throw new RestartNotSupportedException("Restarting the master JVM is not supported in JenkinsRule-based tests"); + } + Lifecycle lifecycle = Lifecycle.get(); + lifecycle.verifyRestartable(); + return lifecycle; + } + /** * Performs a restart. */ public void restart() throws RestartNotSupportedException { - final Lifecycle lifecycle = Lifecycle.get(); - lifecycle.verifyRestartable(); // verify that Jenkins is restartable + final Lifecycle lifecycle = restartableLifecycle(); servletContext.setAttribute("app", new HudsonIsRestarting()); new Thread("restart thread") { @@ -4171,7 +4246,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve // give some time for the browser to load the "reloading" page Thread.sleep(5000); - LOGGER.severe(String.format("Restarting VM as requested by %s",exitUser)); + LOGGER.info(String.format("Restarting VM as requested by %s",exitUser)); for (RestartListener listener : RestartListener.all()) listener.onRestart(); lifecycle.restart(); @@ -4187,8 +4262,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * @since 1.332 */ public void safeRestart() throws RestartNotSupportedException { - final Lifecycle lifecycle = Lifecycle.get(); - lifecycle.verifyRestartable(); // verify that Jenkins is restartable + final Lifecycle lifecycle = restartableLifecycle(); // Quiet down so that we won't launch new builds. isQuietingDown = true; @@ -4208,7 +4282,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve // give some time for the browser to load the "reloading" page LOGGER.info("Restart in 10 seconds"); Thread.sleep(10000); - LOGGER.severe(String.format("Restarting VM as requested by %s",exitUser)); + LOGGER.info(String.format("Restarting VM as requested by %s",exitUser)); for (RestartListener listener : RestartListener.all()) listener.onRestart(); lifecycle.restart(); @@ -4255,8 +4329,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RequirePOST public void doExit( StaplerRequest req, StaplerResponse rsp ) throws IOException { checkPermission(ADMINISTER); - LOGGER.severe(String.format("Shutting down VM as requested by %s from %s", - getAuthentication().getName(), req!=null?req.getRemoteAddr():"???")); if (rsp!=null) { rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); @@ -4265,10 +4337,22 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } } - cleanUp(); - System.exit(0); - } + new Thread("exit thread") { + @Override + public void run() { + try { + ACL.impersonate(ACL.SYSTEM); + LOGGER.info(String.format("Shutting down VM as requested by %s from %s", + getAuthentication().getName(), req != null ? req.getRemoteAddr() : "???")); + cleanUp(); + System.exit(0); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to shut down Jenkins", e); + } + } + }.start(); + } /** * Shutdown the system safely. @@ -4286,7 +4370,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve public void run() { try { ACL.impersonate(ACL.SYSTEM); - LOGGER.severe(String.format("Shutting down VM as requested by %s from %s", + LOGGER.info(String.format("Shutting down VM as requested by %s from %s", exitUser, exitAddr)); // Wait 'til we have no active executors. doQuietDown(true, 0); @@ -4526,7 +4610,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RestrictedSince("2.37") @Deprecated public FormValidation doCheckURIEncoding(StaplerRequest request) throws IOException { - return ExtensionList.lookup(URICheckEncodingMonitor.class).get(0).doCheckURIEncoding(request); + return ExtensionList.lookupSingleton(URICheckEncodingMonitor.class).doCheckURIEncoding(request); } /** @@ -4536,7 +4620,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RestrictedSince("2.37") @Deprecated public static boolean isCheckURIEncodingEnabled() { - return ExtensionList.lookup(URICheckEncodingMonitor.class).get(0).isCheckEnabled(); + return ExtensionList.lookupSingleton(URICheckEncodingMonitor.class).isCheckEnabled(); } /** @@ -4593,7 +4677,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } /** - * Exposes the current user to /me URL. + * Exposes the current user to {@code /me} URL. */ public User getMe() { User u = User.current(); @@ -4609,6 +4693,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * Plugins who wish to contribute boxes on the side panel can add widgets * by {@code getWidgets().add(new MyWidget())} from {@link Plugin#start()}. */ + @StaplerDispatchable // some plugins use this to add views to widgets public List getWidgets() { return widgets; } @@ -4920,7 +5005,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } String ver = props.getProperty("version"); if(ver==null) ver = UNCOMPUTED_VERSION; - if(Main.isDevelopmentMode && "${build.version}".equals(ver)) { + if(Main.isDevelopmentMode && "${project.version}".equals(ver)) { // in dev mode, unable to get version (ahem Eclipse) try { File dir = new File(".").getAbsoluteFile(); @@ -5005,7 +5090,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (idx > 0) { return new VersionNumber(versionString.substring(0,idx)); } - } catch (NumberFormatException _) { + } catch (NumberFormatException ignored) { // fall through } @@ -5074,6 +5159,35 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ private static final String WORKSPACE_DIRNAME = Configuration.getStringConfigParameter("workspaceDirName", "workspace"); + /** + * Default value of job's builds dir. + * @see #getRawBuildsDir() + */ + private static final String DEFAULT_BUILDS_DIR = "${ITEM_ROOTDIR}/builds"; + /** + * Old layout for workspaces. + * @see #DEFAULT_WORKSPACES_DIR + */ + private static final String OLD_DEFAULT_WORKSPACES_DIR = "${ITEM_ROOTDIR}/" + WORKSPACE_DIRNAME; + + /** + * Default value for the workspace's directories layout. + * @see #workspaceDir + */ + private static final String DEFAULT_WORKSPACES_DIR = "${JENKINS_HOME}/workspace/${ITEM_FULL_NAME}"; + + /** + * System property name to set {@link #buildsDir}. + * @see #getRawBuildsDir() + */ + static final String BUILDS_DIR_PROP = Jenkins.class.getName() + ".buildsDir"; + + /** + * System property name to set {@link #workspaceDir}. + * @see #getRawWorkspaceDir() + */ + static final String WORKSPACES_DIR_PROP = Jenkins.class.getName() + ".workspacesDir"; + /** * Automatically try to launch an agent when Jenkins is initialized or a new agent computer is created. */ diff --git a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java index 981ef9a3a665cf6ff6373c93860fc731160a5f64..263b482b0f5a443bb811994100b59f96a50188b8 100644 --- a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java +++ b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java @@ -3,9 +3,12 @@ package jenkins.model; import hudson.Extension; import hudson.Util; import hudson.XmlFile; +import hudson.model.PersistentDescriptor; import hudson.util.FormValidation; import hudson.util.XStream2; import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.QueryParameter; import javax.mail.internet.AddressException; @@ -29,7 +32,7 @@ import javax.annotation.Nonnull; * @since 1.494 */ @Extension @Symbol("location") -public class JenkinsLocationConfiguration extends GlobalConfiguration { +public class JenkinsLocationConfiguration extends GlobalConfiguration implements PersistentDescriptor { /** * @deprecated replaced by {@link #jenkinsUrl} */ @@ -41,12 +44,20 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { // just to suppress warnings private transient String charset,useSsl; - public static @CheckForNull JenkinsLocationConfiguration get() { - return GlobalConfiguration.all().get(JenkinsLocationConfiguration.class); + public static @Nonnull JenkinsLocationConfiguration get() { + return GlobalConfiguration.all().getInstance(JenkinsLocationConfiguration.class); } - - public JenkinsLocationConfiguration() { - load(); + + /** + * Gets local configuration. For explanation when it could die, see {@link #get()} + */ + @Restricted(NoExternalUse.class) + public static @Nonnull JenkinsLocationConfiguration getOrDie(){ + JenkinsLocationConfiguration config = JenkinsLocationConfiguration.get(); + if (config == null) { + throw new IllegalStateException("JenkinsLocationConfiguration instance is missing. Probably the Jenkins instance is not fully loaded at this time."); + } + return config; } @Override @@ -57,7 +68,7 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { if(!file.exists()) { XStream2 xs = new XStream2(); xs.addCompatibilityAlias("hudson.tasks.Mailer$DescriptorImpl",JenkinsLocationConfiguration.class); - file = new XmlFile(xs,new File(Jenkins.getInstance().getRootDir(),"hudson.tasks.Mailer.xml")); + file = new XmlFile(xs,new File(Jenkins.get().getRootDir(),"hudson.tasks.Mailer.xml")); if (file.exists()) { try { file.unmarshal(this); @@ -119,7 +130,7 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { */ private void updateSecureSessionFlag() { try { - ServletContext context = Jenkins.getInstance().servletContext; + ServletContext context = Jenkins.get().servletContext; Method m; try { m = context.getClass().getMethod("getSessionCookieConfig"); @@ -146,7 +157,7 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { } /** - * Checks the URL in global.jelly + * Checks the URL in {@code global.jelly} */ public FormValidation doCheckUrl(@QueryParameter String value) { if(value.startsWith("http://localhost")) diff --git a/core/src/main/java/jenkins/model/MasterBuildConfiguration.java b/core/src/main/java/jenkins/model/MasterBuildConfiguration.java index 6ae797adf2a0addc4c7984d498292b539267f099..e656a6d9bf4eeb92553b18d8020b5581cbdc13ae 100644 --- a/core/src/main/java/jenkins/model/MasterBuildConfiguration.java +++ b/core/src/main/java/jenkins/model/MasterBuildConfiguration.java @@ -39,18 +39,23 @@ import java.io.IOException; @Extension(ordinal=500) @Symbol("masterBuild") public class MasterBuildConfiguration extends GlobalConfiguration { public int getNumExecutors() { - return Jenkins.getInstance().getNumExecutors(); + return Jenkins.get().getNumExecutors(); } public String getLabelString() { - return Jenkins.getInstance().getLabelString(); + return Jenkins.get().getLabelString(); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { - Jenkins j = Jenkins.getInstance(); + Jenkins j = Jenkins.get(); try { // for compatibility reasons, this value is stored in Jenkins + String num = json.getString("numExecutors"); + if (!num.matches("\\d+")) { + throw new FormException(Messages.Hudson_Computer_IncorrectNumberOfExecutors(),"numExecutors"); + } + j.setNumExecutors(json.getInt("numExecutors")); if (req.hasParameter("master.mode")) j.setMode(Mode.valueOf(req.getParameter("master.mode"))); diff --git a/core/src/main/java/jenkins/model/NewViewLink.java b/core/src/main/java/jenkins/model/NewViewLink.java new file mode 100644 index 0000000000000000000000000000000000000000..618dc826dfdc125064f13c5709127e25d515ccde --- /dev/null +++ b/core/src/main/java/jenkins/model/NewViewLink.java @@ -0,0 +1,57 @@ +package jenkins.model; + +import com.google.common.annotations.VisibleForTesting; +import hudson.Extension; +import hudson.model.Action; +import hudson.model.TransientViewActionFactory; +import hudson.model.View; +import java.util.Collections; +import java.util.List; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +public class NewViewLink extends TransientViewActionFactory { + + @VisibleForTesting + static final String ICON_FILE_NAME = "folder"; + @VisibleForTesting + public static final String URL_NAME = "newView"; + + @Override + public List createFor(View v) { + return Collections.singletonList(new Action() { + + @Override + public String getIconFileName() { + if (!hasPermission(v)) { + return null; + } + + return ICON_FILE_NAME; + } + + @Override + public String getDisplayName() { + if (!hasPermission(v)) { + return null; + } + + return Messages.NewViewLink_NewView(); + } + + @Override + public String getUrlName() { + String urlName = Jenkins.getInstance().getRootUrl() + URL_NAME; + return urlName; + } + + private boolean hasPermission(View view) { + return view.hasPermission(View.CREATE); + } + + }); + } +} diff --git a/core/src/main/java/jenkins/model/Nodes.java b/core/src/main/java/jenkins/model/Nodes.java index 2b1116bb86b1f802bb356f45bbe15886c74fb648..24f9ac7431c0adef6cbd72dc6c11402b53087423 100644 --- a/core/src/main/java/jenkins/model/Nodes.java +++ b/core/src/main/java/jenkins/model/Nodes.java @@ -127,7 +127,8 @@ public class Nodes implements Saveable { * @throws IOException if the list of nodes could not be persisted. */ public void addNode(final @Nonnull Node node) throws IOException { - if (node != nodes.get(node.getNodeName())) { + Node oldNode = nodes.get(node.getNodeName()); + if (node != oldNode) { // TODO we should not need to lock the queue for adding nodes but until we have a way to update the // computer list for just the new node Queue.withLock(new Runnable() { @@ -139,7 +140,21 @@ public class Nodes implements Saveable { } }); // TODO there is a theoretical race whereby the node instance is updated/removed after lock release - persistNode(node); + try { + persistNode(node); + } catch (IOException | RuntimeException e) { + // JENKINS-50599: If persisting the node throws an exception, we need to remove the node from + // memory before propagating the exception. + Queue.withLock(new Runnable() { + @Override + public void run() { + nodes.compute(node.getNodeName(), (ignoredNodeName, ignoredNode) -> oldNode); + jenkins.updateComputerList(); + jenkins.trimLabels(); + } + }); + throw e; + } NodeListener.fireOnCreated(node); } } diff --git a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java index b0b9677690b9c46a17e5dcc5cba6b69fbf877e97..e83f3526efb6614e2b63ee6c75efaaab77cdcd4d 100644 --- a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java +++ b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java @@ -54,6 +54,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import javax.servlet.ServletException; import static javax.servlet.http.HttpServletResponse.SC_CREATED; @@ -190,7 +191,7 @@ public abstract class ParameterizedJobMixIn & Param @SuppressWarnings("deprecation") public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException { if (delay == null) { - delay = new TimeDuration(asJob().getQuietPeriod()); + delay=new TimeDuration(TimeUnit.MILLISECONDS.convert(asJob().getQuietPeriod(), TimeUnit.SECONDS)); } if (!asJob().isBuildable()) { @@ -213,7 +214,7 @@ public abstract class ParameterizedJobMixIn & Param } - Queue.Item item = Jenkins.getInstance().getQueue().schedule2(asJob(), delay.getTime(), getBuildCause(asJob(), req)).getItem(); + Queue.Item item = Jenkins.getInstance().getQueue().schedule2(asJob(), delay.getTimeInSeconds(), getBuildCause(asJob(), req)).getItem(); if (item != null) { rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl()); } else { @@ -377,29 +378,11 @@ public abstract class ParameterizedJobMixIn & Param */ Map> getTriggers(); - /** - * @deprecated use {@link #scheduleBuild(Cause)} - */ - @Deprecated - @Override - default boolean scheduleBuild() { - return getParameterizedJobMixIn().scheduleBuild(); - } - @Override default boolean scheduleBuild(Cause c) { return getParameterizedJobMixIn().scheduleBuild(c); } - /** - * @deprecated use {@link #scheduleBuild(int, Cause)} - */ - @Deprecated - @Override - default boolean scheduleBuild(int quietPeriod) { - return getParameterizedJobMixIn().scheduleBuild(quietPeriod); - } - @Override default boolean scheduleBuild(int quietPeriod, Cause c) { return getParameterizedJobMixIn().scheduleBuild(quietPeriod, c); diff --git a/core/src/main/java/jenkins/model/RenameAction.java b/core/src/main/java/jenkins/model/RenameAction.java new file mode 100644 index 0000000000000000000000000000000000000000..45ddbd5290401125c247e1da57462a5d00f8971e --- /dev/null +++ b/core/src/main/java/jenkins/model/RenameAction.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.model; + +import hudson.Extension; +import hudson.model.AbstractItem; +import hudson.model.Action; +import java.util.Collection; +import java.util.Collections; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public class RenameAction implements Action { + + @Override + public String getIconFileName() { + return "notepad.png"; + } + + @Override + public String getDisplayName() { + return "Rename"; + } + + @Override + public String getUrlName() { + return "confirm-rename"; + } + + @Extension + public static class TransientActionFactoryImpl extends TransientActionFactory { + + @Override + public Class type() { + return AbstractItem.class; + } + + @Override + public Collection createFor(AbstractItem target) { + if (target.isNameEditable()) { + return Collections.singleton(new RenameAction()); + } else { + return Collections.emptyList(); + } + } + } +} diff --git a/core/src/main/java/jenkins/model/RunAction2.java b/core/src/main/java/jenkins/model/RunAction2.java index b5443bcccb19b7df05a20dd814c1c413c05f6e1a..1f180e5b9919db964e518640fb5ee18e0e938893 100644 --- a/core/src/main/java/jenkins/model/RunAction2.java +++ b/core/src/main/java/jenkins/model/RunAction2.java @@ -29,6 +29,7 @@ import hudson.model.Run; /** * Optional interface for {@link Action}s that add themselves to a {@link Run}. + * You may keep a {@code transient} reference to an owning build, restored in {@link #onLoad}. * @since 1.519, 1.509.3 */ public interface RunAction2 extends Action { diff --git a/core/src/main/java/jenkins/model/SimplePageDecorator.java b/core/src/main/java/jenkins/model/SimplePageDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..f655ff271b97a161c084ef026c79c0a430f25c0d --- /dev/null +++ b/core/src/main/java/jenkins/model/SimplePageDecorator.java @@ -0,0 +1,78 @@ +/* + * The MIT License + * + * Copyright 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.model; + +import hudson.ExtensionPoint; +import hudson.model.Describable; +import hudson.model.Descriptor; + +import java.util.List; + +/** + * Participates in the rendering of the login page + * + *

+ * This class provides a few hooks to augment the HTML of the login page. + * + * @since 2.128 + */ +public class SimplePageDecorator extends Descriptor implements ExtensionPoint, Describable { + + protected SimplePageDecorator() { + super(self()); + } + + @Override + public final Descriptor getDescriptor() { + return this; + } + /** + * Obtains the URL of this object, excluding the context path. + * + *

+ * Every {@link SimplePageDecorator} is bound to URL via {@link Jenkins#getDescriptor()}. + * This method returns such an URL. + */ + public final String getUrl() { + return "descriptor/"+clazz.getName(); + } + + /** + * Returns all login page decorators. + * @since 2.156 + */ + public static List all() { + return Jenkins.get().getDescriptorList(SimplePageDecorator.class); + } + + /** + * The first found LoginDecarator, there can only be one. + * @return the first found {@link SimplePageDecorator} + */ + public static SimplePageDecorator first(){ + List decorators = all(); + return decorators.isEmpty() ? null : decorators.get(0); + } + +} diff --git a/core/src/main/java/jenkins/model/lazy/AbstractLazyLoadRunMap.java b/core/src/main/java/jenkins/model/lazy/AbstractLazyLoadRunMap.java index a1f2eaecc32ba686bb4af7ed261c581a57ba8bb8..b1d7ae6afe98f60466a97dd35a8ea6ef86b26495 100644 --- a/core/src/main/java/jenkins/model/lazy/AbstractLazyLoadRunMap.java +++ b/core/src/main/java/jenkins/model/lazy/AbstractLazyLoadRunMap.java @@ -29,10 +29,9 @@ import hudson.model.RunMap; import java.io.File; import java.io.IOException; import java.util.AbstractMap; -import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; @@ -41,6 +40,8 @@ import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; + +import jenkins.util.MemoryReductionUtil; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -189,7 +190,7 @@ public abstract class AbstractLazyLoadRunMap extends AbstractMap i String[] kids = dir.list(); if (kids == null) { // the job may have just been created - kids = EMPTY_STRING_ARRAY; + kids = MemoryReductionUtil.EMPTY_STRING_ARRAY; } SortedIntList list = new SortedIntList(kids.length / 2); for (String s : kids) { @@ -336,9 +337,9 @@ public abstract class AbstractLazyLoadRunMap extends AbstractMap i return null; case DESC: // TODO again could be made more efficient - List reversed = new ArrayList(numberOnDisk); - Collections.reverse(reversed); - for (int m : reversed) { + ListIterator iterator = numberOnDisk.listIterator(numberOnDisk.size()); + while(iterator.hasPrevious()) { + int m = iterator.previous(); if (m > n) { continue; } @@ -587,8 +588,6 @@ public abstract class AbstractLazyLoadRunMap extends AbstractMap i ASC, DESC, EXACT } - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static final SortedMap EMPTY_SORTED_MAP = Collections.unmodifiableSortedMap(new TreeMap()); static final Logger LOGGER = Logger.getLogger(AbstractLazyLoadRunMap.class.getName()); diff --git a/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java b/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java index 7ed081657283862c29d17d52d16fab93a1b7e914..c99dd40de67399cfadd0538f193fdeef6a7ce5a3 100644 --- a/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java +++ b/core/src/main/java/jenkins/model/queue/AsynchronousExecution.java @@ -39,6 +39,7 @@ import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import jenkins.model.Jenkins; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; /** @@ -61,7 +62,7 @@ public abstract class AsynchronousExecution extends RuntimeException { /** * Initially null, and usually stays null. - * If {@link #completed} is called before {@link #setExecutor}, then either {@link #NULL} for success, or the error. + * If {@link #completed} is called before {@link #setExecutorWithoutCompleting}, then either {@link #NULL} for success, or the error. */ @GuardedBy("this") private @CheckForNull Throwable result; @@ -98,7 +99,7 @@ public abstract class AsynchronousExecution extends RuntimeException { /** * Obtains the associated executor. - * @return Associated Executor. May be {@code null} if {@link #setExecutor(hudson.model.Executor)} + * @return Associated Executor. May be {@code null} if {@link #setExecutorWithoutCompleting(hudson.model.Executor)} * has not been called yet. */ @CheckForNull @@ -106,13 +107,26 @@ public abstract class AsynchronousExecution extends RuntimeException { return executor; } + /** + * Set the executor without notifying it about task completion. + * The caller must also call {@link #maybeComplete()} + * after releasing any problematic locks. + */ @Restricted(NoExternalUse.class) - public synchronized final void setExecutor(@Nonnull Executor executor) { - assert this.executor==null; - + public synchronized final void setExecutorWithoutCompleting(@Nonnull Executor executor) { + assert this.executor == null; this.executor = executor; - if (result!=null) { - executor.completedAsynchronous( result!=NULL ? result : null ); + } + + /** + * If there is a pending completion notification, deliver it to the executor. + * Must be called after {@link #setExecutorWithoutCompleting(Executor)}. + */ + @Restricted(NoExternalUse.class) + public synchronized final void maybeComplete() { + assert this.executor != null; + if (result != null) { + executor.completedAsynchronous(result != NULL ? result : null); result = null; } } diff --git a/core/src/main/java/jenkins/model/queue/ItemDeletion.java b/core/src/main/java/jenkins/model/queue/ItemDeletion.java index 32d55c6023d94aaf1a69b5caebc1ab26dcf79493..c8bef94b0bb0705b31bec0c0faa06b987570aa0e 100644 --- a/core/src/main/java/jenkins/model/queue/ItemDeletion.java +++ b/core/src/main/java/jenkins/model/queue/ItemDeletion.java @@ -41,7 +41,7 @@ import javax.annotation.concurrent.GuardedBy; /** * A {@link Queue.QueueDecisionHandler} that blocks items being deleted from entering the queue. * - * @since TODO + * @since 2.55 */ @Extension public class ItemDeletion extends Queue.QueueDecisionHandler { diff --git a/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java b/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java index b8d0ebe4a5f0b6b7d8c9afe7ae252d68429cc734..477aa34afd57000f4e638bf079c107ca86739519 100644 --- a/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java +++ b/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java @@ -1,24 +1,23 @@ package jenkins.mvn; import hudson.Extension; +import hudson.model.PersistentDescriptor; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.tools.ToolConfigurationCategory; import org.jenkinsci.Symbol; +import javax.annotation.Nonnull; + //as close as it gets to the global Maven Project configuration @Extension(ordinal = 50) @Symbol("maven") -public class GlobalMavenConfig extends GlobalConfiguration { +public class GlobalMavenConfig extends GlobalConfiguration implements PersistentDescriptor { private SettingsProvider settingsProvider; private GlobalSettingsProvider globalSettingsProvider; - public GlobalMavenConfig() { - load(); - } - @Override - public ToolConfigurationCategory getCategory() { + public @Nonnull ToolConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(ToolConfigurationCategory.class); } @@ -40,8 +39,8 @@ public class GlobalMavenConfig extends GlobalConfiguration { return settingsProvider != null ? settingsProvider : new DefaultSettingsProvider(); } - public static GlobalMavenConfig get() { - return GlobalConfiguration.all().get(GlobalMavenConfig.class); + public static @Nonnull GlobalMavenConfig get() { + return GlobalConfiguration.all().getInstance(GlobalMavenConfig.class); } } diff --git a/core/src/main/java/jenkins/org/apache/commons/validator/routines/DomainValidator.java b/core/src/main/java/jenkins/org/apache/commons/validator/routines/DomainValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..e23c8f15a1cc7be718660c5f0557b5a1000e4ee4 --- /dev/null +++ b/core/src/main/java/jenkins/org/apache/commons/validator/routines/DomainValidator.java @@ -0,0 +1,2074 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Copied from commons-validator:commons-validator:1.6, with [PATCH] modifications */ +package jenkins.org.apache.commons.validator.routines; + +import jenkins.util.MemoryReductionUtil; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.Serializable; +import java.net.IDN; +import java.util.Arrays; +import java.util.Locale; + +/** + *

Domain name validation routines.

+ * + *

+ * This validator provides methods for validating Internet domain names + * and top-level domains. + *

+ * + *

Domain names are evaluated according + * to the standards RFC1034, + * section 3, and RFC1123, + * section 2.1. No accommodation is provided for the specialized needs of + * other applications; if the domain name has been URL-encoded, for example, + * validation will fail even though the equivalent plaintext version of the + * same name would have passed. + *

+ * + *

+ * Validation is also provided for top-level domains (TLDs) as defined and + * maintained by the Internet Assigned Numbers Authority (IANA): + *

+ * + *
    + *
  • {@link #isValidInfrastructureTld} - validates infrastructure TLDs + * (.arpa, etc.)
  • + *
  • {@link #isValidGenericTld} - validates generic TLDs + * (.com, .org, etc.)
  • + *
  • {@link #isValidCountryCodeTld} - validates country code TLDs + * (.us, .uk, .cn, etc.)
  • + *
+ * + *

+ * (NOTE: This class does not provide IP address lookup for domain names or + * methods to ensure that a given domain name matches a specific IP; see + * {@link java.net.InetAddress} for that functionality.) + *

+ * + * @version $Revision: 1781829 $ + * @since Validator 1.4 + */ +//[PATCH] +@Restricted(NoExternalUse.class) +// end of [PATCH] +public class DomainValidator implements Serializable { + + private static final int MAX_DOMAIN_LENGTH = 253; + + private static final long serialVersionUID = -4407125112880174009L; + + // Regular expression strings for hostnames (derived from RFC2396 and RFC 1123) + + // RFC2396: domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // Max 63 characters + private static final String DOMAIN_LABEL_REGEX = "\\p{Alnum}(?>[\\p{Alnum}-]{0,61}\\p{Alnum})?"; + + // RFC2396 toplabel = alpha | alpha *( alphanum | "-" ) alphanum + // Max 63 characters + private static final String TOP_LABEL_REGEX = "\\p{Alpha}(?>[\\p{Alnum}-]{0,61}\\p{Alnum})?"; + + // RFC2396 hostname = *( domainlabel "." ) toplabel [ "." ] + // Note that the regex currently requires both a domain label and a top level label, whereas + // the RFC does not. This is because the regex is used to detect if a TLD is present. + // If the match fails, input is checked against DOMAIN_LABEL_REGEX (hostnameRegex) + // RFC1123 sec 2.1 allows hostnames to start with a digit + private static final String DOMAIN_NAME_REGEX = + "^(?:" + DOMAIN_LABEL_REGEX + "\\.)+" + "(" + TOP_LABEL_REGEX + ")\\.?$"; + + private final boolean allowLocal; + + /** + * Singleton instance of this validator, which + * doesn't consider local addresses as valid. + */ + private static final DomainValidator DOMAIN_VALIDATOR = new DomainValidator(false); + + /** + * Singleton instance of this validator, which does + * consider local addresses valid. + */ + private static final DomainValidator DOMAIN_VALIDATOR_WITH_LOCAL = new DomainValidator(true); + + /** + * RegexValidator for matching domains. + */ + private final RegexValidator domainRegex = + new RegexValidator(DOMAIN_NAME_REGEX); + /** + * RegexValidator for matching a local hostname + */ + // RFC1123 sec 2.1 allows hostnames to start with a digit + private final RegexValidator hostnameRegex = + new RegexValidator(DOMAIN_LABEL_REGEX); + + /** + * Returns the singleton instance of this validator. It + * will not consider local addresses as valid. + * @return the singleton instance of this validator + */ + public static synchronized DomainValidator getInstance() { + inUse = true; + return DOMAIN_VALIDATOR; + } + + /** + * Returns the singleton instance of this validator, + * with local validation as required. + * @param allowLocal Should local addresses be considered valid? + * @return the singleton instance of this validator + */ + public static synchronized DomainValidator getInstance(boolean allowLocal) { + inUse = true; + if(allowLocal) { + return DOMAIN_VALIDATOR_WITH_LOCAL; + } + return DOMAIN_VALIDATOR; + } + + /** Private constructor. */ + private DomainValidator(boolean allowLocal) { + this.allowLocal = allowLocal; + } + + /** + * Returns true if the specified String parses + * as a valid domain name with a recognized top-level domain. + * The parsing is case-insensitive. + * @param domain the parameter to check for domain name syntax + * @return true if the parameter is a valid domain name + */ + public boolean isValid(String domain) { + if (domain == null) { + return false; + } + domain = unicodeToASCII(domain); + // hosts must be equally reachable via punycode and Unicode; + // Unicode is never shorter than punycode, so check punycode + // if domain did not convert, then it will be caught by ASCII + // checks in the regexes below + if (domain.length() > MAX_DOMAIN_LENGTH) { + return false; + } + String[] groups = domainRegex.match(domain); + if (groups != null && groups.length > 0) { + return isValidTld(groups[0]); + } + return allowLocal && hostnameRegex.isValid(domain); + } + + // package protected for unit test access + // must agree with isValidRootUrl() above + final boolean isValidDomainSyntax(String domain) { + if (domain == null) { + return false; + } + domain = unicodeToASCII(domain); + // hosts must be equally reachable via punycode and Unicode; + // Unicode is never shorter than punycode, so check punycode + // if domain did not convert, then it will be caught by ASCII + // checks in the regexes below + if (domain.length() > MAX_DOMAIN_LENGTH) { + return false; + } + String[] groups = domainRegex.match(domain); + return (groups != null && groups.length > 0) + || hostnameRegex.isValid(domain); + } + + /** + * Returns true if the specified String matches any + * IANA-defined top-level domain. Leading dots are ignored if present. + * The search is case-insensitive. + * @param tld the parameter to check for TLD status, not null + * @return true if the parameter is a TLD + */ + public boolean isValidTld(String tld) { + tld = unicodeToASCII(tld); + if(allowLocal && isValidLocalTld(tld)) { + return true; + } + return isValidInfrastructureTld(tld) + || isValidGenericTld(tld) + || isValidCountryCodeTld(tld); + } + + /** + * Returns true if the specified String matches any + * IANA-defined infrastructure top-level domain. Leading dots are + * ignored if present. The search is case-insensitive. + * @param iTld the parameter to check for infrastructure TLD status, not null + * @return true if the parameter is an infrastructure TLD + */ + public boolean isValidInfrastructureTld(String iTld) { + final String key = chompLeadingDot(unicodeToASCII(iTld).toLowerCase(Locale.ENGLISH)); + return arrayContains(INFRASTRUCTURE_TLDS, key); + } + + /** + * Returns true if the specified String matches any + * IANA-defined generic top-level domain. Leading dots are ignored + * if present. The search is case-insensitive. + * @param gTld the parameter to check for generic TLD status, not null + * @return true if the parameter is a generic TLD + */ + public boolean isValidGenericTld(String gTld) { + final String key = chompLeadingDot(unicodeToASCII(gTld).toLowerCase(Locale.ENGLISH)); + return (arrayContains(GENERIC_TLDS, key) || arrayContains(genericTLDsPlus, key)) + && !arrayContains(genericTLDsMinus, key); + } + + /** + * Returns true if the specified String matches any + * IANA-defined country code top-level domain. Leading dots are + * ignored if present. The search is case-insensitive. + * @param ccTld the parameter to check for country code TLD status, not null + * @return true if the parameter is a country code TLD + */ + public boolean isValidCountryCodeTld(String ccTld) { + final String key = chompLeadingDot(unicodeToASCII(ccTld).toLowerCase(Locale.ENGLISH)); + return (arrayContains(COUNTRY_CODE_TLDS, key) || arrayContains(countryCodeTLDsPlus, key)) + && !arrayContains(countryCodeTLDsMinus, key); + } + + /** + * Returns true if the specified String matches any + * widely used "local" domains (localhost or localdomain). Leading dots are + * ignored if present. The search is case-insensitive. + * @param lTld the parameter to check for local TLD status, not null + * @return true if the parameter is an local TLD + */ + public boolean isValidLocalTld(String lTld) { + final String key = chompLeadingDot(unicodeToASCII(lTld).toLowerCase(Locale.ENGLISH)); + return arrayContains(LOCAL_TLDS, key); + } + + private String chompLeadingDot(String str) { + if (str.startsWith(".")) { + return str.substring(1); + } + return str; + } + + // --------------------------------------------- + // ----- TLDs defined by IANA + // ----- Authoritative and comprehensive list at: + // ----- http://data.iana.org/TLD/tlds-alpha-by-domain.txt + + // Note that the above list is in UPPER case. + // The code currently converts strings to lower case (as per the tables below) + + // IANA also provide an HTML list at http://www.iana.org/domains/root/db + // Note that this contains several country code entries which are NOT in + // the text file. These all have the "Not assigned" in the "Sponsoring Organisation" column + // For example (as of 2015-01-02): + // .bl country-code Not assigned + // .um country-code Not assigned + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static final String[] INFRASTRUCTURE_TLDS = new String[] { + "arpa", // internet infrastructure + }; + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static final String[] GENERIC_TLDS = new String[] { + // Taken from Version 2017020400, Last Updated Sat Feb 4 07:07:01 2017 UTC + "aaa", // aaa American Automobile Association, Inc. + "aarp", // aarp AARP + "abarth", // abarth Fiat Chrysler Automobiles N.V. + "abb", // abb ABB Ltd + "abbott", // abbott Abbott Laboratories, Inc. + "abbvie", // abbvie AbbVie Inc. + "abc", // abc Disney Enterprises, Inc. + "able", // able Able Inc. + "abogado", // abogado Top Level Domain Holdings Limited + "abudhabi", // abudhabi Abu Dhabi Systems and Information Centre + "academy", // academy Half Oaks, LLC + "accenture", // accenture Accenture plc + "accountant", // accountant dot Accountant Limited + "accountants", // accountants Knob Town, LLC + "aco", // aco ACO Severin Ahlmann GmbH & Co. KG + "active", // active The Active Network, Inc + "actor", // actor United TLD Holdco Ltd. + "adac", // adac Allgemeiner Deutscher Automobil-Club e.V. (ADAC) + "ads", // ads Charleston Road Registry Inc. + "adult", // adult ICM Registry AD LLC + "aeg", // aeg Aktiebolaget Electrolux + "aero", // aero Societe Internationale de Telecommunications Aeronautique (SITA INC USA) + "aetna", // aetna Aetna Life Insurance Company + "afamilycompany", // afamilycompany Johnson Shareholdings, Inc. + "afl", // afl Australian Football League + "agakhan", // agakhan Fondation Aga Khan (Aga Khan Foundation) + "agency", // agency Steel Falls, LLC + "aig", // aig American International Group, Inc. + "aigo", // aigo aigo Digital Technology Co,Ltd. + "airbus", // airbus Airbus S.A.S. + "airforce", // airforce United TLD Holdco Ltd. + "airtel", // airtel Bharti Airtel Limited + "akdn", // akdn Fondation Aga Khan (Aga Khan Foundation) + "alfaromeo", // alfaromeo Fiat Chrysler Automobiles N.V. + "alibaba", // alibaba Alibaba Group Holding Limited + "alipay", // alipay Alibaba Group Holding Limited + "allfinanz", // allfinanz Allfinanz Deutsche Vermögensberatung Aktiengesellschaft + "allstate", // allstate Allstate Fire and Casualty Insurance Company + "ally", // ally Ally Financial Inc. + "alsace", // alsace REGION D ALSACE + "alstom", // alstom ALSTOM + "americanexpress", // americanexpress American Express Travel Related Services Company, Inc. + "americanfamily", // americanfamily AmFam, Inc. + "amex", // amex American Express Travel Related Services Company, Inc. + "amfam", // amfam AmFam, Inc. + "amica", // amica Amica Mutual Insurance Company + "amsterdam", // amsterdam Gemeente Amsterdam + "analytics", // analytics Campus IP LLC + "android", // android Charleston Road Registry Inc. + "anquan", // anquan QIHOO 360 TECHNOLOGY CO. LTD. + "anz", // anz Australia and New Zealand Banking Group Limited + "aol", // aol AOL Inc. + "apartments", // apartments June Maple, LLC + "app", // app Charleston Road Registry Inc. + "apple", // apple Apple Inc. + "aquarelle", // aquarelle Aquarelle.com + "aramco", // aramco Aramco Services Company + "archi", // archi STARTING DOT LIMITED + "army", // army United TLD Holdco Ltd. + "art", // art UK Creative Ideas Limited + "arte", // arte Association Relative à la Télévision Européenne G.E.I.E. + "asda", // asda Wal-Mart Stores, Inc. + "asia", // asia DotAsia Organisation Ltd. + "associates", // associates Baxter Hill, LLC + "athleta", // athleta The Gap, Inc. + "attorney", // attorney United TLD Holdco, Ltd + "auction", // auction United TLD HoldCo, Ltd. + "audi", // audi AUDI Aktiengesellschaft + "audible", // audible Amazon Registry Services, Inc. + "audio", // audio Uniregistry, Corp. + "auspost", // auspost Australian Postal Corporation + "author", // author Amazon Registry Services, Inc. + "auto", // auto Uniregistry, Corp. + "autos", // autos DERAutos, LLC + "avianca", // avianca Aerovias del Continente Americano S.A. Avianca + "aws", // aws Amazon Registry Services, Inc. + "axa", // axa AXA SA + "azure", // azure Microsoft Corporation + "baby", // baby Johnson & Johnson Services, Inc. + "baidu", // baidu Baidu, Inc. + "banamex", // banamex Citigroup Inc. + "bananarepublic", // bananarepublic The Gap, Inc. + "band", // band United TLD Holdco, Ltd + "bank", // bank fTLD Registry Services, LLC + "bar", // bar Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable + "barcelona", // barcelona Municipi de Barcelona + "barclaycard", // barclaycard Barclays Bank PLC + "barclays", // barclays Barclays Bank PLC + "barefoot", // barefoot Gallo Vineyards, Inc. + "bargains", // bargains Half Hallow, LLC + "baseball", // baseball MLB Advanced Media DH, LLC + "basketball", // basketball Fédération Internationale de Basketball (FIBA) + "bauhaus", // bauhaus Werkhaus GmbH + "bayern", // bayern Bayern Connect GmbH + "bbc", // bbc British Broadcasting Corporation + "bbt", // bbt BB&T Corporation + "bbva", // bbva BANCO BILBAO VIZCAYA ARGENTARIA, S.A. + "bcg", // bcg The Boston Consulting Group, Inc. + "bcn", // bcn Municipi de Barcelona + "beats", // beats Beats Electronics, LLC + "beauty", // beauty L'Oréal + "beer", // beer Top Level Domain Holdings Limited + "bentley", // bentley Bentley Motors Limited + "berlin", // berlin dotBERLIN GmbH & Co. KG + "best", // best BestTLD Pty Ltd + "bestbuy", // bestbuy BBY Solutions, Inc. + "bet", // bet Afilias plc + "bharti", // bharti Bharti Enterprises (Holding) Private Limited + "bible", // bible American Bible Society + "bid", // bid dot Bid Limited + "bike", // bike Grand Hollow, LLC + "bing", // bing Microsoft Corporation + "bingo", // bingo Sand Cedar, LLC + "bio", // bio STARTING DOT LIMITED + "biz", // biz Neustar, Inc. + "black", // black Afilias Limited + "blackfriday", // blackfriday Uniregistry, Corp. + "blanco", // blanco BLANCO GmbH + Co KG + "blockbuster", // blockbuster Dish DBS Corporation + "blog", // blog Knock Knock WHOIS There, LLC + "bloomberg", // bloomberg Bloomberg IP Holdings LLC + "blue", // blue Afilias Limited + "bms", // bms Bristol-Myers Squibb Company + "bmw", // bmw Bayerische Motoren Werke Aktiengesellschaft + "bnl", // bnl Banca Nazionale del Lavoro + "bnpparibas", // bnpparibas BNP Paribas + "boats", // boats DERBoats, LLC + "boehringer", // boehringer Boehringer Ingelheim International GmbH + "bofa", // bofa NMS Services, Inc. + "bom", // bom Núcleo de Informação e Coordenação do Ponto BR - NIC.br + "bond", // bond Bond University Limited + "boo", // boo Charleston Road Registry Inc. + "book", // book Amazon Registry Services, Inc. + "booking", // booking Booking.com B.V. + "boots", // boots THE BOOTS COMPANY PLC + "bosch", // bosch Robert Bosch GMBH + "bostik", // bostik Bostik SA + "boston", // boston Boston TLD Management, LLC + "bot", // bot Amazon Registry Services, Inc. + "boutique", // boutique Over Galley, LLC + "box", // box NS1 Limited + "bradesco", // bradesco Banco Bradesco S.A. + "bridgestone", // bridgestone Bridgestone Corporation + "broadway", // broadway Celebrate Broadway, Inc. + "broker", // broker DOTBROKER REGISTRY LTD + "brother", // brother Brother Industries, Ltd. + "brussels", // brussels DNS.be vzw + "budapest", // budapest Top Level Domain Holdings Limited + "bugatti", // bugatti Bugatti International SA + "build", // build Plan Bee LLC + "builders", // builders Atomic Madison, LLC + "business", // business Spring Cross, LLC + "buy", // buy Amazon Registry Services, INC + "buzz", // buzz DOTSTRATEGY CO. + "bzh", // bzh Association www.bzh + "cab", // cab Half Sunset, LLC + "cafe", // cafe Pioneer Canyon, LLC + "cal", // cal Charleston Road Registry Inc. + "call", // call Amazon Registry Services, Inc. + "calvinklein", // calvinklein PVH gTLD Holdings LLC + "cam", // cam AC Webconnecting Holding B.V. + "camera", // camera Atomic Maple, LLC + "camp", // camp Delta Dynamite, LLC + "cancerresearch", // cancerresearch Australian Cancer Research Foundation + "canon", // canon Canon Inc. + "capetown", // capetown ZA Central Registry NPC trading as ZA Central Registry + "capital", // capital Delta Mill, LLC + "capitalone", // capitalone Capital One Financial Corporation + "car", // car Cars Registry Limited + "caravan", // caravan Caravan International, Inc. + "cards", // cards Foggy Hollow, LLC + "care", // care Goose Cross, LLC + "career", // career dotCareer LLC + "careers", // careers Wild Corner, LLC + "cars", // cars Uniregistry, Corp. + "cartier", // cartier Richemont DNS Inc. + "casa", // casa Top Level Domain Holdings Limited + "case", // case CNH Industrial N.V. + "caseih", // caseih CNH Industrial N.V. + "cash", // cash Delta Lake, LLC + "casino", // casino Binky Sky, LLC + "cat", // cat Fundacio puntCAT + "catering", // catering New Falls. LLC + "catholic", // catholic Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) + "cba", // cba COMMONWEALTH BANK OF AUSTRALIA + "cbn", // cbn The Christian Broadcasting Network, Inc. + "cbre", // cbre CBRE, Inc. + "cbs", // cbs CBS Domains Inc. + "ceb", // ceb The Corporate Executive Board Company + "center", // center Tin Mill, LLC + "ceo", // ceo CEOTLD Pty Ltd + "cern", // cern European Organization for Nuclear Research ("CERN") + "cfa", // cfa CFA Institute + "cfd", // cfd DOTCFD REGISTRY LTD + "chanel", // chanel Chanel International B.V. + "channel", // channel Charleston Road Registry Inc. + "chase", // chase JPMorgan Chase & Co. + "chat", // chat Sand Fields, LLC + "cheap", // cheap Sand Cover, LLC + "chintai", // chintai CHINTAI Corporation + "chloe", // chloe Richemont DNS Inc. + "christmas", // christmas Uniregistry, Corp. + "chrome", // chrome Charleston Road Registry Inc. + "chrysler", // chrysler FCA US LLC. + "church", // church Holly Fileds, LLC + "cipriani", // cipriani Hotel Cipriani Srl + "circle", // circle Amazon Registry Services, Inc. + "cisco", // cisco Cisco Technology, Inc. + "citadel", // citadel Citadel Domain LLC + "citi", // citi Citigroup Inc. + "citic", // citic CITIC Group Corporation + "city", // city Snow Sky, LLC + "cityeats", // cityeats Lifestyle Domain Holdings, Inc. + "claims", // claims Black Corner, LLC + "cleaning", // cleaning Fox Shadow, LLC + "click", // click Uniregistry, Corp. + "clinic", // clinic Goose Park, LLC + "clinique", // clinique The Estée Lauder Companies Inc. + "clothing", // clothing Steel Lake, LLC + "cloud", // cloud ARUBA S.p.A. + "club", // club .CLUB DOMAINS, LLC + "clubmed", // clubmed Club Méditerranée S.A. + "coach", // coach Koko Island, LLC + "codes", // codes Puff Willow, LLC + "coffee", // coffee Trixy Cover, LLC + "college", // college XYZ.COM LLC + "cologne", // cologne NetCologne Gesellschaft für Telekommunikation mbH + "com", // com VeriSign Global Registry Services + "comcast", // comcast Comcast IP Holdings I, LLC + "commbank", // commbank COMMONWEALTH BANK OF AUSTRALIA + "community", // community Fox Orchard, LLC + "company", // company Silver Avenue, LLC + "compare", // compare iSelect Ltd + "computer", // computer Pine Mill, LLC + "comsec", // comsec VeriSign, Inc. + "condos", // condos Pine House, LLC + "construction", // construction Fox Dynamite, LLC + "consulting", // consulting United TLD Holdco, LTD. + "contact", // contact Top Level Spectrum, Inc. + "contractors", // contractors Magic Woods, LLC + "cooking", // cooking Top Level Domain Holdings Limited + "cookingchannel", // cookingchannel Lifestyle Domain Holdings, Inc. + "cool", // cool Koko Lake, LLC + "coop", // coop DotCooperation LLC + "corsica", // corsica Collectivité Territoriale de Corse + "country", // country Top Level Domain Holdings Limited + "coupon", // coupon Amazon Registry Services, Inc. + "coupons", // coupons Black Island, LLC + "courses", // courses OPEN UNIVERSITIES AUSTRALIA PTY LTD + "credit", // credit Snow Shadow, LLC + "creditcard", // creditcard Binky Frostbite, LLC + "creditunion", // creditunion CUNA Performance Resources, LLC + "cricket", // cricket dot Cricket Limited + "crown", // crown Crown Equipment Corporation + "crs", // crs Federated Co-operatives Limited + "cruise", // cruise Viking River Cruises (Bermuda) Ltd. + "cruises", // cruises Spring Way, LLC + "csc", // csc Alliance-One Services, Inc. + "cuisinella", // cuisinella SALM S.A.S. + "cymru", // cymru Nominet UK + "cyou", // cyou Beijing Gamease Age Digital Technology Co., Ltd. + "dabur", // dabur Dabur India Limited + "dad", // dad Charleston Road Registry Inc. + "dance", // dance United TLD Holdco Ltd. + "data", // data Dish DBS Corporation + "date", // date dot Date Limited + "dating", // dating Pine Fest, LLC + "datsun", // datsun NISSAN MOTOR CO., LTD. + "day", // day Charleston Road Registry Inc. + "dclk", // dclk Charleston Road Registry Inc. + "dds", // dds Minds + Machines Group Limited + "deal", // deal Amazon Registry Services, Inc. + "dealer", // dealer Dealer Dot Com, Inc. + "deals", // deals Sand Sunset, LLC + "degree", // degree United TLD Holdco, Ltd + "delivery", // delivery Steel Station, LLC + "dell", // dell Dell Inc. + "deloitte", // deloitte Deloitte Touche Tohmatsu + "delta", // delta Delta Air Lines, Inc. + "democrat", // democrat United TLD Holdco Ltd. + "dental", // dental Tin Birch, LLC + "dentist", // dentist United TLD Holdco, Ltd + "desi", // desi Desi Networks LLC + "design", // design Top Level Design, LLC + "dev", // dev Charleston Road Registry Inc. + "dhl", // dhl Deutsche Post AG + "diamonds", // diamonds John Edge, LLC + "diet", // diet Uniregistry, Corp. + "digital", // digital Dash Park, LLC + "direct", // direct Half Trail, LLC + "directory", // directory Extra Madison, LLC + "discount", // discount Holly Hill, LLC + "discover", // discover Discover Financial Services + "dish", // dish Dish DBS Corporation + "diy", // diy Lifestyle Domain Holdings, Inc. + "dnp", // dnp Dai Nippon Printing Co., Ltd. + "docs", // docs Charleston Road Registry Inc. + "doctor", // doctor Brice Trail, LLC + "dodge", // dodge FCA US LLC. + "dog", // dog Koko Mill, LLC + "doha", // doha Communications Regulatory Authority (CRA) + "domains", // domains Sugar Cross, LLC +// "doosan", // doosan Doosan Corporation (retired) + "dot", // dot Dish DBS Corporation + "download", // download dot Support Limited + "drive", // drive Charleston Road Registry Inc. + "dtv", // dtv Dish DBS Corporation + "dubai", // dubai Dubai Smart Government Department + "duck", // duck Johnson Shareholdings, Inc. + "dunlop", // dunlop The Goodyear Tire & Rubber Company + "duns", // duns The Dun & Bradstreet Corporation + "dupont", // dupont E. I. du Pont de Nemours and Company + "durban", // durban ZA Central Registry NPC trading as ZA Central Registry + "dvag", // dvag Deutsche Vermögensberatung Aktiengesellschaft DVAG + "dvr", // dvr Hughes Satellite Systems Corporation + "earth", // earth Interlink Co., Ltd. + "eat", // eat Charleston Road Registry Inc. + "eco", // eco Big Room Inc. + "edeka", // edeka EDEKA Verband kaufmännischer Genossenschaften e.V. + "edu", // edu EDUCAUSE + "education", // education Brice Way, LLC + "email", // email Spring Madison, LLC + "emerck", // emerck Merck KGaA + "energy", // energy Binky Birch, LLC + "engineer", // engineer United TLD Holdco Ltd. + "engineering", // engineering Romeo Canyon + "enterprises", // enterprises Snow Oaks, LLC + "epost", // epost Deutsche Post AG + "epson", // epson Seiko Epson Corporation + "equipment", // equipment Corn Station, LLC + "ericsson", // ericsson Telefonaktiebolaget L M Ericsson + "erni", // erni ERNI Group Holding AG + "esq", // esq Charleston Road Registry Inc. + "estate", // estate Trixy Park, LLC + "esurance", // esurance Esurance Insurance Company + "eurovision", // eurovision European Broadcasting Union (EBU) + "eus", // eus Puntueus Fundazioa + "events", // events Pioneer Maple, LLC + "everbank", // everbank EverBank + "exchange", // exchange Spring Falls, LLC + "expert", // expert Magic Pass, LLC + "exposed", // exposed Victor Beach, LLC + "express", // express Sea Sunset, LLC + "extraspace", // extraspace Extra Space Storage LLC + "fage", // fage Fage International S.A. + "fail", // fail Atomic Pipe, LLC + "fairwinds", // fairwinds FairWinds Partners, LLC + "faith", // faith dot Faith Limited + "family", // family United TLD Holdco Ltd. + "fan", // fan Asiamix Digital Ltd + "fans", // fans Asiamix Digital Limited + "farm", // farm Just Maple, LLC + "farmers", // farmers Farmers Insurance Exchange + "fashion", // fashion Top Level Domain Holdings Limited + "fast", // fast Amazon Registry Services, Inc. + "fedex", // fedex Federal Express Corporation + "feedback", // feedback Top Level Spectrum, Inc. + "ferrari", // ferrari Fiat Chrysler Automobiles N.V. + "ferrero", // ferrero Ferrero Trading Lux S.A. + "fiat", // fiat Fiat Chrysler Automobiles N.V. + "fidelity", // fidelity Fidelity Brokerage Services LLC + "fido", // fido Rogers Communications Canada Inc. + "film", // film Motion Picture Domain Registry Pty Ltd + "final", // final Núcleo de Informação e Coordenação do Ponto BR - NIC.br + "finance", // finance Cotton Cypress, LLC + "financial", // financial Just Cover, LLC + "fire", // fire Amazon Registry Services, Inc. + "firestone", // firestone Bridgestone Corporation + "firmdale", // firmdale Firmdale Holdings Limited + "fish", // fish Fox Woods, LLC + "fishing", // fishing Top Level Domain Holdings Limited + "fit", // fit Minds + Machines Group Limited + "fitness", // fitness Brice Orchard, LLC + "flickr", // flickr Yahoo! Domain Services Inc. + "flights", // flights Fox Station, LLC + "flir", // flir FLIR Systems, Inc. + "florist", // florist Half Cypress, LLC + "flowers", // flowers Uniregistry, Corp. +// "flsmidth", // flsmidth FLSmidth A/S retired 2016-07-22 + "fly", // fly Charleston Road Registry Inc. + "foo", // foo Charleston Road Registry Inc. + "food", // food Lifestyle Domain Holdings, Inc. + "foodnetwork", // foodnetwork Lifestyle Domain Holdings, Inc. + "football", // football Foggy Farms, LLC + "ford", // ford Ford Motor Company + "forex", // forex DOTFOREX REGISTRY LTD + "forsale", // forsale United TLD Holdco, LLC + "forum", // forum Fegistry, LLC + "foundation", // foundation John Dale, LLC + "fox", // fox FOX Registry, LLC + "free", // free Amazon Registry Services, Inc. + "fresenius", // fresenius Fresenius Immobilien-Verwaltungs-GmbH + "frl", // frl FRLregistry B.V. + "frogans", // frogans OP3FT + "frontdoor", // frontdoor Lifestyle Domain Holdings, Inc. + "frontier", // frontier Frontier Communications Corporation + "ftr", // ftr Frontier Communications Corporation + "fujitsu", // fujitsu Fujitsu Limited + "fujixerox", // fujixerox Xerox DNHC LLC + "fun", // fun DotSpace, Inc. + "fund", // fund John Castle, LLC + "furniture", // furniture Lone Fields, LLC + "futbol", // futbol United TLD Holdco, Ltd. + "fyi", // fyi Silver Tigers, LLC + "gal", // gal Asociación puntoGAL + "gallery", // gallery Sugar House, LLC + "gallo", // gallo Gallo Vineyards, Inc. + "gallup", // gallup Gallup, Inc. + "game", // game Uniregistry, Corp. + "games", // games United TLD Holdco Ltd. + "gap", // gap The Gap, Inc. + "garden", // garden Top Level Domain Holdings Limited + "gbiz", // gbiz Charleston Road Registry Inc. + "gdn", // gdn Joint Stock Company "Navigation-information systems" + "gea", // gea GEA Group Aktiengesellschaft + "gent", // gent COMBELL GROUP NV/SA + "genting", // genting Resorts World Inc. Pte. Ltd. + "george", // george Wal-Mart Stores, Inc. + "ggee", // ggee GMO Internet, Inc. + "gift", // gift Uniregistry, Corp. + "gifts", // gifts Goose Sky, LLC + "gives", // gives United TLD Holdco Ltd. + "giving", // giving Giving Limited + "glade", // glade Johnson Shareholdings, Inc. + "glass", // glass Black Cover, LLC + "gle", // gle Charleston Road Registry Inc. + "global", // global Dot Global Domain Registry Limited + "globo", // globo Globo Comunicação e Participações S.A + "gmail", // gmail Charleston Road Registry Inc. + "gmbh", // gmbh Extra Dynamite, LLC + "gmo", // gmo GMO Internet, Inc. + "gmx", // gmx 1&1 Mail & Media GmbH + "godaddy", // godaddy Go Daddy East, LLC + "gold", // gold June Edge, LLC + "goldpoint", // goldpoint YODOBASHI CAMERA CO.,LTD. + "golf", // golf Lone Falls, LLC + "goo", // goo NTT Resonant Inc. + "goodhands", // goodhands Allstate Fire and Casualty Insurance Company + "goodyear", // goodyear The Goodyear Tire & Rubber Company + "goog", // goog Charleston Road Registry Inc. + "google", // google Charleston Road Registry Inc. + "gop", // gop Republican State Leadership Committee, Inc. + "got", // got Amazon Registry Services, Inc. + "gov", // gov General Services Administration Attn: QTDC, 2E08 (.gov Domain Registration) + "grainger", // grainger Grainger Registry Services, LLC + "graphics", // graphics Over Madison, LLC + "gratis", // gratis Pioneer Tigers, LLC + "green", // green Afilias Limited + "gripe", // gripe Corn Sunset, LLC + "group", // group Romeo Town, LLC + "guardian", // guardian The Guardian Life Insurance Company of America + "gucci", // gucci Guccio Gucci S.p.a. + "guge", // guge Charleston Road Registry Inc. + "guide", // guide Snow Moon, LLC + "guitars", // guitars Uniregistry, Corp. + "guru", // guru Pioneer Cypress, LLC + "hair", // hair L'Oreal + "hamburg", // hamburg Hamburg Top-Level-Domain GmbH + "hangout", // hangout Charleston Road Registry Inc. + "haus", // haus United TLD Holdco, LTD. + "hbo", // hbo HBO Registry Services, Inc. + "hdfc", // hdfc HOUSING DEVELOPMENT FINANCE CORPORATION LIMITED + "hdfcbank", // hdfcbank HDFC Bank Limited + "health", // health DotHealth, LLC + "healthcare", // healthcare Silver Glen, LLC + "help", // help Uniregistry, Corp. + "helsinki", // helsinki City of Helsinki + "here", // here Charleston Road Registry Inc. + "hermes", // hermes Hermes International + "hgtv", // hgtv Lifestyle Domain Holdings, Inc. + "hiphop", // hiphop Uniregistry, Corp. + "hisamitsu", // hisamitsu Hisamitsu Pharmaceutical Co.,Inc. + "hitachi", // hitachi Hitachi, Ltd. + "hiv", // hiv dotHIV gemeinnuetziger e.V. + "hkt", // hkt PCCW-HKT DataCom Services Limited + "hockey", // hockey Half Willow, LLC + "holdings", // holdings John Madison, LLC + "holiday", // holiday Goose Woods, LLC + "homedepot", // homedepot Homer TLC, Inc. + "homegoods", // homegoods The TJX Companies, Inc. + "homes", // homes DERHomes, LLC + "homesense", // homesense The TJX Companies, Inc. + "honda", // honda Honda Motor Co., Ltd. + "honeywell", // honeywell Honeywell GTLD LLC + "horse", // horse Top Level Domain Holdings Limited + "hospital", // hospital Ruby Pike, LLC + "host", // host DotHost Inc. + "hosting", // hosting Uniregistry, Corp. + "hot", // hot Amazon Registry Services, Inc. + "hoteles", // hoteles Travel Reservations SRL + "hotmail", // hotmail Microsoft Corporation + "house", // house Sugar Park, LLC + "how", // how Charleston Road Registry Inc. + "hsbc", // hsbc HSBC Holdings PLC + "htc", // htc HTC corporation + "hughes", // hughes Hughes Satellite Systems Corporation + "hyatt", // hyatt Hyatt GTLD, L.L.C. + "hyundai", // hyundai Hyundai Motor Company + "ibm", // ibm International Business Machines Corporation + "icbc", // icbc Industrial and Commercial Bank of China Limited + "ice", // ice IntercontinentalExchange, Inc. + "icu", // icu One.com A/S + "ieee", // ieee IEEE Global LLC + "ifm", // ifm ifm electronic gmbh +// "iinet", // iinet Connect West Pty. Ltd. (Retired) + "ikano", // ikano Ikano S.A. + "imamat", // imamat Fondation Aga Khan (Aga Khan Foundation) + "imdb", // imdb Amazon Registry Services, Inc. + "immo", // immo Auburn Bloom, LLC + "immobilien", // immobilien United TLD Holdco Ltd. + "industries", // industries Outer House, LLC + "infiniti", // infiniti NISSAN MOTOR CO., LTD. + "info", // info Afilias Limited + "ing", // ing Charleston Road Registry Inc. + "ink", // ink Top Level Design, LLC + "institute", // institute Outer Maple, LLC + "insurance", // insurance fTLD Registry Services LLC + "insure", // insure Pioneer Willow, LLC + "int", // int Internet Assigned Numbers Authority + "intel", // intel Intel Corporation + "international", // international Wild Way, LLC + "intuit", // intuit Intuit Administrative Services, Inc. + "investments", // investments Holly Glen, LLC + "ipiranga", // ipiranga Ipiranga Produtos de Petroleo S.A. + "irish", // irish Dot-Irish LLC + "iselect", // iselect iSelect Ltd + "ismaili", // ismaili Fondation Aga Khan (Aga Khan Foundation) + "ist", // ist Istanbul Metropolitan Municipality + "istanbul", // istanbul Istanbul Metropolitan Municipality / Medya A.S. + "itau", // itau Itau Unibanco Holding S.A. + "itv", // itv ITV Services Limited + "iveco", // iveco CNH Industrial N.V. + "iwc", // iwc Richemont DNS Inc. + "jaguar", // jaguar Jaguar Land Rover Ltd + "java", // java Oracle Corporation + "jcb", // jcb JCB Co., Ltd. + "jcp", // jcp JCP Media, Inc. + "jeep", // jeep FCA US LLC. + "jetzt", // jetzt New TLD Company AB + "jewelry", // jewelry Wild Bloom, LLC + "jio", // jio Affinity Names, Inc. + "jlc", // jlc Richemont DNS Inc. + "jll", // jll Jones Lang LaSalle Incorporated + "jmp", // jmp Matrix IP LLC + "jnj", // jnj Johnson & Johnson Services, Inc. + "jobs", // jobs Employ Media LLC + "joburg", // joburg ZA Central Registry NPC trading as ZA Central Registry + "jot", // jot Amazon Registry Services, Inc. + "joy", // joy Amazon Registry Services, Inc. + "jpmorgan", // jpmorgan JPMorgan Chase & Co. + "jprs", // jprs Japan Registry Services Co., Ltd. + "juegos", // juegos Uniregistry, Corp. + "juniper", // juniper JUNIPER NETWORKS, INC. + "kaufen", // kaufen United TLD Holdco Ltd. + "kddi", // kddi KDDI CORPORATION + "kerryhotels", // kerryhotels Kerry Trading Co. Limited + "kerrylogistics", // kerrylogistics Kerry Trading Co. Limited + "kerryproperties", // kerryproperties Kerry Trading Co. Limited + "kfh", // kfh Kuwait Finance House + "kia", // kia KIA MOTORS CORPORATION + "kim", // kim Afilias Limited + "kinder", // kinder Ferrero Trading Lux S.A. + "kindle", // kindle Amazon Registry Services, Inc. + "kitchen", // kitchen Just Goodbye, LLC + "kiwi", // kiwi DOT KIWI LIMITED + "koeln", // koeln NetCologne Gesellschaft für Telekommunikation mbH + "komatsu", // komatsu Komatsu Ltd. + "kosher", // kosher Kosher Marketing Assets LLC + "kpmg", // kpmg KPMG International Cooperative (KPMG International Genossenschaft) + "kpn", // kpn Koninklijke KPN N.V. + "krd", // krd KRG Department of Information Technology + "kred", // kred KredTLD Pty Ltd + "kuokgroup", // kuokgroup Kerry Trading Co. Limited + "kyoto", // kyoto Academic Institution: Kyoto Jyoho Gakuen + "lacaixa", // lacaixa CAIXA D'ESTALVIS I PENSIONS DE BARCELONA + "ladbrokes", // ladbrokes LADBROKES INTERNATIONAL PLC + "lamborghini", // lamborghini Automobili Lamborghini S.p.A. + "lamer", // lamer The Estée Lauder Companies Inc. + "lancaster", // lancaster LANCASTER + "lancia", // lancia Fiat Chrysler Automobiles N.V. + "lancome", // lancome L'Oréal + "land", // land Pine Moon, LLC + "landrover", // landrover Jaguar Land Rover Ltd + "lanxess", // lanxess LANXESS Corporation + "lasalle", // lasalle Jones Lang LaSalle Incorporated + "lat", // lat ECOM-LAC Federación de Latinoamérica y el Caribe para Internet y el Comercio Electrónico + "latino", // latino Dish DBS Corporation + "latrobe", // latrobe La Trobe University + "law", // law Minds + Machines Group Limited + "lawyer", // lawyer United TLD Holdco, Ltd + "lds", // lds IRI Domain Management, LLC + "lease", // lease Victor Trail, LLC + "leclerc", // leclerc A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc + "lefrak", // lefrak LeFrak Organization, Inc. + "legal", // legal Blue Falls, LLC + "lego", // lego LEGO Juris A/S + "lexus", // lexus TOYOTA MOTOR CORPORATION + "lgbt", // lgbt Afilias Limited + "liaison", // liaison Liaison Technologies, Incorporated + "lidl", // lidl Schwarz Domains und Services GmbH & Co. KG + "life", // life Trixy Oaks, LLC + "lifeinsurance", // lifeinsurance American Council of Life Insurers + "lifestyle", // lifestyle Lifestyle Domain Holdings, Inc. + "lighting", // lighting John McCook, LLC + "like", // like Amazon Registry Services, Inc. + "lilly", // lilly Eli Lilly and Company + "limited", // limited Big Fest, LLC + "limo", // limo Hidden Frostbite, LLC + "lincoln", // lincoln Ford Motor Company + "linde", // linde Linde Aktiengesellschaft + "link", // link Uniregistry, Corp. + "lipsy", // lipsy Lipsy Ltd + "live", // live United TLD Holdco Ltd. + "living", // living Lifestyle Domain Holdings, Inc. + "lixil", // lixil LIXIL Group Corporation + "loan", // loan dot Loan Limited + "loans", // loans June Woods, LLC + "locker", // locker Dish DBS Corporation + "locus", // locus Locus Analytics LLC + "loft", // loft Annco, Inc. + "lol", // lol Uniregistry, Corp. + "london", // london Dot London Domains Limited + "lotte", // lotte Lotte Holdings Co., Ltd. + "lotto", // lotto Afilias Limited + "love", // love Merchant Law Group LLP + "lpl", // lpl LPL Holdings, Inc. + "lplfinancial", // lplfinancial LPL Holdings, Inc. + "ltd", // ltd Over Corner, LLC + "ltda", // ltda InterNetX Corp. + "lundbeck", // lundbeck H. Lundbeck A/S + "lupin", // lupin LUPIN LIMITED + "luxe", // luxe Top Level Domain Holdings Limited + "luxury", // luxury Luxury Partners LLC + "macys", // macys Macys, Inc. + "madrid", // madrid Comunidad de Madrid + "maif", // maif Mutuelle Assurance Instituteur France (MAIF) + "maison", // maison Victor Frostbite, LLC + "makeup", // makeup L'Oréal + "man", // man MAN SE + "management", // management John Goodbye, LLC + "mango", // mango PUNTO FA S.L. + "market", // market Unitied TLD Holdco, Ltd + "marketing", // marketing Fern Pass, LLC + "markets", // markets DOTMARKETS REGISTRY LTD + "marriott", // marriott Marriott Worldwide Corporation + "marshalls", // marshalls The TJX Companies, Inc. + "maserati", // maserati Fiat Chrysler Automobiles N.V. + "mattel", // mattel Mattel Sites, Inc. + "mba", // mba Lone Hollow, LLC + "mcd", // mcd McDonald’s Corporation + "mcdonalds", // mcdonalds McDonald’s Corporation + "mckinsey", // mckinsey McKinsey Holdings, Inc. + "med", // med Medistry LLC + "media", // media Grand Glen, LLC + "meet", // meet Afilias Limited + "melbourne", // melbourne The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation + "meme", // meme Charleston Road Registry Inc. + "memorial", // memorial Dog Beach, LLC + "men", // men Exclusive Registry Limited + "menu", // menu Wedding TLD2, LLC + "meo", // meo PT Comunicacoes S.A. + "metlife", // metlife MetLife Services and Solutions, LLC + "miami", // miami Top Level Domain Holdings Limited + "microsoft", // microsoft Microsoft Corporation + "mil", // mil DoD Network Information Center + "mini", // mini Bayerische Motoren Werke Aktiengesellschaft + "mint", // mint Intuit Administrative Services, Inc. + "mit", // mit Massachusetts Institute of Technology + "mitsubishi", // mitsubishi Mitsubishi Corporation + "mlb", // mlb MLB Advanced Media DH, LLC + "mls", // mls The Canadian Real Estate Association + "mma", // mma MMA IARD + "mobi", // mobi Afilias Technologies Limited dba dotMobi + "mobile", // mobile Dish DBS Corporation + "mobily", // mobily GreenTech Consultancy Company W.L.L. + "moda", // moda United TLD Holdco Ltd. + "moe", // moe Interlink Co., Ltd. + "moi", // moi Amazon Registry Services, Inc. + "mom", // mom Uniregistry, Corp. + "monash", // monash Monash University + "money", // money Outer McCook, LLC + "monster", // monster Monster Worldwide, Inc. + "montblanc", // montblanc Richemont DNS Inc. + "mopar", // mopar FCA US LLC. + "mormon", // mormon IRI Domain Management, LLC ("Applicant") + "mortgage", // mortgage United TLD Holdco, Ltd + "moscow", // moscow Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) + "moto", // moto Motorola Trademark Holdings, LLC + "motorcycles", // motorcycles DERMotorcycles, LLC + "mov", // mov Charleston Road Registry Inc. + "movie", // movie New Frostbite, LLC + "movistar", // movistar Telefónica S.A. + "msd", // msd MSD Registry Holdings, Inc. + "mtn", // mtn MTN Dubai Limited + "mtpc", // mtpc Mitsubishi Tanabe Pharma Corporation + "mtr", // mtr MTR Corporation Limited + "museum", // museum Museum Domain Management Association + "mutual", // mutual Northwestern Mutual MU TLD Registry, LLC +// "mutuelle", // mutuelle Fédération Nationale de la Mutualité Française (Retired) + "nab", // nab National Australia Bank Limited + "nadex", // nadex Nadex Domains, Inc + "nagoya", // nagoya GMO Registry, Inc. + "name", // name VeriSign Information Services, Inc. + "nationwide", // nationwide Nationwide Mutual Insurance Company + "natura", // natura NATURA COSMÉTICOS S.A. + "navy", // navy United TLD Holdco Ltd. + "nba", // nba NBA REGISTRY, LLC + "nec", // nec NEC Corporation + "net", // net VeriSign Global Registry Services + "netbank", // netbank COMMONWEALTH BANK OF AUSTRALIA + "netflix", // netflix Netflix, Inc. + "network", // network Trixy Manor, LLC + "neustar", // neustar NeuStar, Inc. + "new", // new Charleston Road Registry Inc. + "newholland", // newholland CNH Industrial N.V. + "news", // news United TLD Holdco Ltd. + "next", // next Next plc + "nextdirect", // nextdirect Next plc + "nexus", // nexus Charleston Road Registry Inc. + "nfl", // nfl NFL Reg Ops LLC + "ngo", // ngo Public Interest Registry + "nhk", // nhk Japan Broadcasting Corporation (NHK) + "nico", // nico DWANGO Co., Ltd. + "nike", // nike NIKE, Inc. + "nikon", // nikon NIKON CORPORATION + "ninja", // ninja United TLD Holdco Ltd. + "nissan", // nissan NISSAN MOTOR CO., LTD. + "nissay", // nissay Nippon Life Insurance Company + "nokia", // nokia Nokia Corporation + "northwesternmutual", // northwesternmutual Northwestern Mutual Registry, LLC + "norton", // norton Symantec Corporation + "now", // now Amazon Registry Services, Inc. + "nowruz", // nowruz Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. + "nowtv", // nowtv Starbucks (HK) Limited + "nra", // nra NRA Holdings Company, INC. + "nrw", // nrw Minds + Machines GmbH + "ntt", // ntt NIPPON TELEGRAPH AND TELEPHONE CORPORATION + "nyc", // nyc The City of New York by and through the New York City Department of Information Technology & Telecommunications + "obi", // obi OBI Group Holding SE & Co. KGaA + "observer", // observer Top Level Spectrum, Inc. + "off", // off Johnson Shareholdings, Inc. + "office", // office Microsoft Corporation + "okinawa", // okinawa BusinessRalliart inc. + "olayan", // olayan Crescent Holding GmbH + "olayangroup", // olayangroup Crescent Holding GmbH + "oldnavy", // oldnavy The Gap, Inc. + "ollo", // ollo Dish DBS Corporation + "omega", // omega The Swatch Group Ltd + "one", // one One.com A/S + "ong", // ong Public Interest Registry + "onl", // onl I-REGISTRY Ltd., Niederlassung Deutschland + "online", // online DotOnline Inc. + "onyourside", // onyourside Nationwide Mutual Insurance Company + "ooo", // ooo INFIBEAM INCORPORATION LIMITED + "open", // open American Express Travel Related Services Company, Inc. + "oracle", // oracle Oracle Corporation + "orange", // orange Orange Brand Services Limited + "org", // org Public Interest Registry (PIR) + "organic", // organic Afilias Limited + "orientexpress", // orientexpress Orient Express + "origins", // origins The Estée Lauder Companies Inc. + "osaka", // osaka Interlink Co., Ltd. + "otsuka", // otsuka Otsuka Holdings Co., Ltd. + "ott", // ott Dish DBS Corporation + "ovh", // ovh OVH SAS + "page", // page Charleston Road Registry Inc. + "pamperedchef", // pamperedchef The Pampered Chef, Ltd. + "panasonic", // panasonic Panasonic Corporation + "panerai", // panerai Richemont DNS Inc. + "paris", // paris City of Paris + "pars", // pars Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. + "partners", // partners Magic Glen, LLC + "parts", // parts Sea Goodbye, LLC + "party", // party Blue Sky Registry Limited + "passagens", // passagens Travel Reservations SRL + "pay", // pay Amazon Registry Services, Inc. + "pccw", // pccw PCCW Enterprises Limited + "pet", // pet Afilias plc + "pfizer", // pfizer Pfizer Inc. + "pharmacy", // pharmacy National Association of Boards of Pharmacy + "philips", // philips Koninklijke Philips N.V. + "phone", // phone Dish DBS Corporation + "photo", // photo Uniregistry, Corp. + "photography", // photography Sugar Glen, LLC + "photos", // photos Sea Corner, LLC + "physio", // physio PhysBiz Pty Ltd + "piaget", // piaget Richemont DNS Inc. + "pics", // pics Uniregistry, Corp. + "pictet", // pictet Pictet Europe S.A. + "pictures", // pictures Foggy Sky, LLC + "pid", // pid Top Level Spectrum, Inc. + "pin", // pin Amazon Registry Services, Inc. + "ping", // ping Ping Registry Provider, Inc. + "pink", // pink Afilias Limited + "pioneer", // pioneer Pioneer Corporation + "pizza", // pizza Foggy Moon, LLC + "place", // place Snow Galley, LLC + "play", // play Charleston Road Registry Inc. + "playstation", // playstation Sony Computer Entertainment Inc. + "plumbing", // plumbing Spring Tigers, LLC + "plus", // plus Sugar Mill, LLC + "pnc", // pnc PNC Domain Co., LLC + "pohl", // pohl Deutsche Vermögensberatung Aktiengesellschaft DVAG + "poker", // poker Afilias Domains No. 5 Limited + "politie", // politie Politie Nederland + "porn", // porn ICM Registry PN LLC + "post", // post Universal Postal Union + "pramerica", // pramerica Prudential Financial, Inc. + "praxi", // praxi Praxi S.p.A. + "press", // press DotPress Inc. + "prime", // prime Amazon Registry Services, Inc. + "pro", // pro Registry Services Corporation dba RegistryPro + "prod", // prod Charleston Road Registry Inc. + "productions", // productions Magic Birch, LLC + "prof", // prof Charleston Road Registry Inc. + "progressive", // progressive Progressive Casualty Insurance Company + "promo", // promo Afilias plc + "properties", // properties Big Pass, LLC + "property", // property Uniregistry, Corp. + "protection", // protection XYZ.COM LLC + "pru", // pru Prudential Financial, Inc. + "prudential", // prudential Prudential Financial, Inc. + "pub", // pub United TLD Holdco Ltd. + "pwc", // pwc PricewaterhouseCoopers LLP + "qpon", // qpon dotCOOL, Inc. + "quebec", // quebec PointQuébec Inc + "quest", // quest Quest ION Limited + "qvc", // qvc QVC, Inc. + "racing", // racing Premier Registry Limited + "radio", // radio European Broadcasting Union (EBU) + "raid", // raid Johnson Shareholdings, Inc. + "read", // read Amazon Registry Services, Inc. + "realestate", // realestate dotRealEstate LLC + "realtor", // realtor Real Estate Domains LLC + "realty", // realty Fegistry, LLC + "recipes", // recipes Grand Island, LLC + "red", // red Afilias Limited + "redstone", // redstone Redstone Haute Couture Co., Ltd. + "redumbrella", // redumbrella Travelers TLD, LLC + "rehab", // rehab United TLD Holdco Ltd. + "reise", // reise Foggy Way, LLC + "reisen", // reisen New Cypress, LLC + "reit", // reit National Association of Real Estate Investment Trusts, Inc. + "reliance", // reliance Reliance Industries Limited + "ren", // ren Beijing Qianxiang Wangjing Technology Development Co., Ltd. + "rent", // rent XYZ.COM LLC + "rentals", // rentals Big Hollow,LLC + "repair", // repair Lone Sunset, LLC + "report", // report Binky Glen, LLC + "republican", // republican United TLD Holdco Ltd. + "rest", // rest Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable + "restaurant", // restaurant Snow Avenue, LLC + "review", // review dot Review Limited + "reviews", // reviews United TLD Holdco, Ltd. + "rexroth", // rexroth Robert Bosch GMBH + "rich", // rich I-REGISTRY Ltd., Niederlassung Deutschland + "richardli", // richardli Pacific Century Asset Management (HK) Limited + "ricoh", // ricoh Ricoh Company, Ltd. + "rightathome", // rightathome Johnson Shareholdings, Inc. + "ril", // ril Reliance Industries Limited + "rio", // rio Empresa Municipal de Informática SA - IPLANRIO + "rip", // rip United TLD Holdco Ltd. + "rmit", // rmit Royal Melbourne Institute of Technology + "rocher", // rocher Ferrero Trading Lux S.A. + "rocks", // rocks United TLD Holdco, LTD. + "rodeo", // rodeo Top Level Domain Holdings Limited + "rogers", // rogers Rogers Communications Canada Inc. + "room", // room Amazon Registry Services, Inc. + "rsvp", // rsvp Charleston Road Registry Inc. + "ruhr", // ruhr regiodot GmbH & Co. KG + "run", // run Snow Park, LLC + "rwe", // rwe RWE AG + "ryukyu", // ryukyu BusinessRalliart inc. + "saarland", // saarland dotSaarland GmbH + "safe", // safe Amazon Registry Services, Inc. + "safety", // safety Safety Registry Services, LLC. + "sakura", // sakura SAKURA Internet Inc. + "sale", // sale United TLD Holdco, Ltd + "salon", // salon Outer Orchard, LLC + "samsclub", // samsclub Wal-Mart Stores, Inc. + "samsung", // samsung SAMSUNG SDS CO., LTD + "sandvik", // sandvik Sandvik AB + "sandvikcoromant", // sandvikcoromant Sandvik AB + "sanofi", // sanofi Sanofi + "sap", // sap SAP AG + "sapo", // sapo PT Comunicacoes S.A. + "sarl", // sarl Delta Orchard, LLC + "sas", // sas Research IP LLC + "save", // save Amazon Registry Services, Inc. + "saxo", // saxo Saxo Bank A/S + "sbi", // sbi STATE BANK OF INDIA + "sbs", // sbs SPECIAL BROADCASTING SERVICE CORPORATION + "sca", // sca SVENSKA CELLULOSA AKTIEBOLAGET SCA (publ) + "scb", // scb The Siam Commercial Bank Public Company Limited ("SCB") + "schaeffler", // schaeffler Schaeffler Technologies AG & Co. KG + "schmidt", // schmidt SALM S.A.S. + "scholarships", // scholarships Scholarships.com, LLC + "school", // school Little Galley, LLC + "schule", // schule Outer Moon, LLC + "schwarz", // schwarz Schwarz Domains und Services GmbH & Co. KG + "science", // science dot Science Limited + "scjohnson", // scjohnson Johnson Shareholdings, Inc. + "scor", // scor SCOR SE + "scot", // scot Dot Scot Registry Limited + "seat", // seat SEAT, S.A. (Sociedad Unipersonal) + "secure", // secure Amazon Registry Services, Inc. + "security", // security XYZ.COM LLC + "seek", // seek Seek Limited + "select", // select iSelect Ltd + "sener", // sener Sener Ingeniería y Sistemas, S.A. + "services", // services Fox Castle, LLC + "ses", // ses SES + "seven", // seven Seven West Media Ltd + "sew", // sew SEW-EURODRIVE GmbH & Co KG + "sex", // sex ICM Registry SX LLC + "sexy", // sexy Uniregistry, Corp. + "sfr", // sfr Societe Francaise du Radiotelephone - SFR + "shangrila", // shangrila Shangri‐La International Hotel Management Limited + "sharp", // sharp Sharp Corporation + "shaw", // shaw Shaw Cablesystems G.P. + "shell", // shell Shell Information Technology International Inc + "shia", // shia Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. + "shiksha", // shiksha Afilias Limited + "shoes", // shoes Binky Galley, LLC + "shop", // shop GMO Registry, Inc. + "shopping", // shopping Over Keep, LLC + "shouji", // shouji QIHOO 360 TECHNOLOGY CO. LTD. + "show", // show Snow Beach, LLC + "showtime", // showtime CBS Domains Inc. + "shriram", // shriram Shriram Capital Ltd. + "silk", // silk Amazon Registry Services, Inc. + "sina", // sina Sina Corporation + "singles", // singles Fern Madison, LLC + "site", // site DotSite Inc. + "ski", // ski STARTING DOT LIMITED + "skin", // skin L'Oréal + "sky", // sky Sky International AG + "skype", // skype Microsoft Corporation + "sling", // sling Hughes Satellite Systems Corporation + "smart", // smart Smart Communications, Inc. (SMART) + "smile", // smile Amazon Registry Services, Inc. + "sncf", // sncf SNCF (Société Nationale des Chemins de fer Francais) + "soccer", // soccer Foggy Shadow, LLC + "social", // social United TLD Holdco Ltd. + "softbank", // softbank SoftBank Group Corp. + "software", // software United TLD Holdco, Ltd + "sohu", // sohu Sohu.com Limited + "solar", // solar Ruby Town, LLC + "solutions", // solutions Silver Cover, LLC + "song", // song Amazon Registry Services, Inc. + "sony", // sony Sony Corporation + "soy", // soy Charleston Road Registry Inc. + "space", // space DotSpace Inc. + "spiegel", // spiegel SPIEGEL-Verlag Rudolf Augstein GmbH & Co. KG + "spot", // spot Amazon Registry Services, Inc. + "spreadbetting", // spreadbetting DOTSPREADBETTING REGISTRY LTD + "srl", // srl InterNetX Corp. + "srt", // srt FCA US LLC. + "stada", // stada STADA Arzneimittel AG + "staples", // staples Staples, Inc. + "star", // star Star India Private Limited + "starhub", // starhub StarHub Limited + "statebank", // statebank STATE BANK OF INDIA + "statefarm", // statefarm State Farm Mutual Automobile Insurance Company + "statoil", // statoil Statoil ASA + "stc", // stc Saudi Telecom Company + "stcgroup", // stcgroup Saudi Telecom Company + "stockholm", // stockholm Stockholms kommun + "storage", // storage Self Storage Company LLC + "store", // store DotStore Inc. + "stream", // stream dot Stream Limited + "studio", // studio United TLD Holdco Ltd. + "study", // study OPEN UNIVERSITIES AUSTRALIA PTY LTD + "style", // style Binky Moon, LLC + "sucks", // sucks Vox Populi Registry Ltd. + "supplies", // supplies Atomic Fields, LLC + "supply", // supply Half Falls, LLC + "support", // support Grand Orchard, LLC + "surf", // surf Top Level Domain Holdings Limited + "surgery", // surgery Tin Avenue, LLC + "suzuki", // suzuki SUZUKI MOTOR CORPORATION + "swatch", // swatch The Swatch Group Ltd + "swiftcover", // swiftcover Swiftcover Insurance Services Limited + "swiss", // swiss Swiss Confederation + "sydney", // sydney State of New South Wales, Department of Premier and Cabinet + "symantec", // symantec Symantec Corporation + "systems", // systems Dash Cypress, LLC + "tab", // tab Tabcorp Holdings Limited + "taipei", // taipei Taipei City Government + "talk", // talk Amazon Registry Services, Inc. + "taobao", // taobao Alibaba Group Holding Limited + "target", // target Target Domain Holdings, LLC + "tatamotors", // tatamotors Tata Motors Ltd + "tatar", // tatar Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" + "tattoo", // tattoo Uniregistry, Corp. + "tax", // tax Storm Orchard, LLC + "taxi", // taxi Pine Falls, LLC + "tci", // tci Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. + "tdk", // tdk TDK Corporation + "team", // team Atomic Lake, LLC + "tech", // tech Dot Tech LLC + "technology", // technology Auburn Falls, LLC + "tel", // tel Telnic Ltd. + "telecity", // telecity TelecityGroup International Limited + "telefonica", // telefonica Telefónica S.A. + "temasek", // temasek Temasek Holdings (Private) Limited + "tennis", // tennis Cotton Bloom, LLC + "teva", // teva Teva Pharmaceutical Industries Limited + "thd", // thd Homer TLC, Inc. + "theater", // theater Blue Tigers, LLC + "theatre", // theatre XYZ.COM LLC + "tiaa", // tiaa Teachers Insurance and Annuity Association of America + "tickets", // tickets Accent Media Limited + "tienda", // tienda Victor Manor, LLC + "tiffany", // tiffany Tiffany and Company + "tips", // tips Corn Willow, LLC + "tires", // tires Dog Edge, LLC + "tirol", // tirol punkt Tirol GmbH + "tjmaxx", // tjmaxx The TJX Companies, Inc. + "tjx", // tjx The TJX Companies, Inc. + "tkmaxx", // tkmaxx The TJX Companies, Inc. + "tmall", // tmall Alibaba Group Holding Limited + "today", // today Pearl Woods, LLC + "tokyo", // tokyo GMO Registry, Inc. + "tools", // tools Pioneer North, LLC + "top", // top Jiangsu Bangning Science & Technology Co.,Ltd. + "toray", // toray Toray Industries, Inc. + "toshiba", // toshiba TOSHIBA Corporation + "total", // total Total SA + "tours", // tours Sugar Station, LLC + "town", // town Koko Moon, LLC + "toyota", // toyota TOYOTA MOTOR CORPORATION + "toys", // toys Pioneer Orchard, LLC + "trade", // trade Elite Registry Limited + "trading", // trading DOTTRADING REGISTRY LTD + "training", // training Wild Willow, LLC + "travel", // travel Tralliance Registry Management Company, LLC. + "travelchannel", // travelchannel Lifestyle Domain Holdings, Inc. + "travelers", // travelers Travelers TLD, LLC + "travelersinsurance", // travelersinsurance Travelers TLD, LLC + "trust", // trust Artemis Internet Inc + "trv", // trv Travelers TLD, LLC + "tube", // tube Latin American Telecom LLC + "tui", // tui TUI AG + "tunes", // tunes Amazon Registry Services, Inc. + "tushu", // tushu Amazon Registry Services, Inc. + "tvs", // tvs T V SUNDRAM IYENGAR & SONS PRIVATE LIMITED + "ubank", // ubank National Australia Bank Limited + "ubs", // ubs UBS AG + "uconnect", // uconnect FCA US LLC. + "unicom", // unicom China United Network Communications Corporation Limited + "university", // university Little Station, LLC + "uno", // uno Dot Latin LLC + "uol", // uol UBN INTERNET LTDA. + "ups", // ups UPS Market Driver, Inc. + "vacations", // vacations Atomic Tigers, LLC + "vana", // vana Lifestyle Domain Holdings, Inc. + "vanguard", // vanguard The Vanguard Group, Inc. + "vegas", // vegas Dot Vegas, Inc. + "ventures", // ventures Binky Lake, LLC + "verisign", // verisign VeriSign, Inc. + "versicherung", // versicherung dotversicherung-registry GmbH + "vet", // vet United TLD Holdco, Ltd + "viajes", // viajes Black Madison, LLC + "video", // video United TLD Holdco, Ltd + "vig", // vig VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe + "viking", // viking Viking River Cruises (Bermuda) Ltd. + "villas", // villas New Sky, LLC + "vin", // vin Holly Shadow, LLC + "vip", // vip Minds + Machines Group Limited + "virgin", // virgin Virgin Enterprises Limited + "visa", // visa Visa Worldwide Pte. Limited + "vision", // vision Koko Station, LLC + "vista", // vista Vistaprint Limited + "vistaprint", // vistaprint Vistaprint Limited + "viva", // viva Saudi Telecom Company + "vivo", // vivo Telefonica Brasil S.A. + "vlaanderen", // vlaanderen DNS.be vzw + "vodka", // vodka Top Level Domain Holdings Limited + "volkswagen", // volkswagen Volkswagen Group of America Inc. + "volvo", // volvo Volvo Holding Sverige Aktiebolag + "vote", // vote Monolith Registry LLC + "voting", // voting Valuetainment Corp. + "voto", // voto Monolith Registry LLC + "voyage", // voyage Ruby House, LLC + "vuelos", // vuelos Travel Reservations SRL + "wales", // wales Nominet UK + "walmart", // walmart Wal-Mart Stores, Inc. + "walter", // walter Sandvik AB + "wang", // wang Zodiac Registry Limited + "wanggou", // wanggou Amazon Registry Services, Inc. + "warman", // warman Weir Group IP Limited + "watch", // watch Sand Shadow, LLC + "watches", // watches Richemont DNS Inc. + "weather", // weather The Weather Channel, LLC + "weatherchannel", // weatherchannel The Weather Channel, LLC + "webcam", // webcam dot Webcam Limited + "weber", // weber Saint-Gobain Weber SA + "website", // website DotWebsite Inc. + "wed", // wed Atgron, Inc. + "wedding", // wedding Top Level Domain Holdings Limited + "weibo", // weibo Sina Corporation + "weir", // weir Weir Group IP Limited + "whoswho", // whoswho Who's Who Registry + "wien", // wien punkt.wien GmbH + "wiki", // wiki Top Level Design, LLC + "williamhill", // williamhill William Hill Organization Limited + "win", // win First Registry Limited + "windows", // windows Microsoft Corporation + "wine", // wine June Station, LLC + "winners", // winners The TJX Companies, Inc. + "wme", // wme William Morris Endeavor Entertainment, LLC + "wolterskluwer", // wolterskluwer Wolters Kluwer N.V. + "woodside", // woodside Woodside Petroleum Limited + "work", // work Top Level Domain Holdings Limited + "works", // works Little Dynamite, LLC + "world", // world Bitter Fields, LLC + "wow", // wow Amazon Registry Services, Inc. + "wtc", // wtc World Trade Centers Association, Inc. + "wtf", // wtf Hidden Way, LLC + "xbox", // xbox Microsoft Corporation + "xerox", // xerox Xerox DNHC LLC + "xfinity", // xfinity Comcast IP Holdings I, LLC + "xihuan", // xihuan QIHOO 360 TECHNOLOGY CO. LTD. + "xin", // xin Elegant Leader Limited + "xn--11b4c3d", // कॉम VeriSign Sarl + "xn--1ck2e1b", // セール Amazon Registry Services, Inc. + "xn--1qqw23a", // 佛山 Guangzhou YU Wei Information Technology Co., Ltd. + "xn--30rr7y", // 慈善 Excellent First Limited + "xn--3bst00m", // 集团 Eagle Horizon Limited + "xn--3ds443g", // 在线 TLD REGISTRY LIMITED + "xn--3oq18vl8pn36a", // 大众汽车 Volkswagen (China) Investment Co., Ltd. + "xn--3pxu8k", // 点看 VeriSign Sarl + "xn--42c2d9a", // คอม VeriSign Sarl + "xn--45q11c", // 八卦 Zodiac Scorpio Limited + "xn--4gbrim", // موقع Suhub Electronic Establishment + "xn--55qw42g", // 公益 China Organizational Name Administration Center + "xn--55qx5d", // 公司 Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center) + "xn--5su34j936bgsg", // 香格里拉 Shangri‐La International Hotel Management Limited + "xn--5tzm5g", // 网站 Global Website TLD Asia Limited + "xn--6frz82g", // 移动 Afilias Limited + "xn--6qq986b3xl", // 我爱你 Tycoon Treasure Limited + "xn--80adxhks", // москва Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) + "xn--80aqecdr1a", // католик Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) + "xn--80asehdb", // онлайн CORE Association + "xn--80aswg", // сайт CORE Association + "xn--8y0a063a", // 联通 China United Network Communications Corporation Limited + "xn--90ae", // бг Imena.BG Plc (NAMES.BG Plc) + "xn--9dbq2a", // קום VeriSign Sarl + "xn--9et52u", // 时尚 RISE VICTORY LIMITED + "xn--9krt00a", // 微博 Sina Corporation + "xn--b4w605ferd", // 淡马锡 Temasek Holdings (Private) Limited + "xn--bck1b9a5dre4c", // ファッション Amazon Registry Services, Inc. + "xn--c1avg", // орг Public Interest Registry + "xn--c2br7g", // नेट VeriSign Sarl + "xn--cck2b3b", // ストア Amazon Registry Services, Inc. + "xn--cg4bki", // 삼성 SAMSUNG SDS CO., LTD + "xn--czr694b", // 商标 HU YI GLOBAL INFORMATION RESOURCES(HOLDING) COMPANY.HONGKONG LIMITED + "xn--czrs0t", // 商店 Wild Island, LLC + "xn--czru2d", // 商城 Zodiac Aquarius Limited + "xn--d1acj3b", // дети The Foundation for Network Initiatives “The Smart Internet” + "xn--eckvdtc9d", // ポイント Amazon Registry Services, Inc. + "xn--efvy88h", // 新闻 Xinhua News Agency Guangdong Branch 新华通讯社广东分社 + "xn--estv75g", // 工行 Industrial and Commercial Bank of China Limited + "xn--fct429k", // 家電 Amazon Registry Services, Inc. + "xn--fhbei", // كوم VeriSign Sarl + "xn--fiq228c5hs", // 中文网 TLD REGISTRY LIMITED + "xn--fiq64b", // 中信 CITIC Group Corporation + "xn--fjq720a", // 娱乐 Will Bloom, LLC + "xn--flw351e", // 谷歌 Charleston Road Registry Inc. + "xn--fzys8d69uvgm", // 電訊盈科 PCCW Enterprises Limited + "xn--g2xx48c", // 购物 Minds + Machines Group Limited + "xn--gckr3f0f", // クラウド Amazon Registry Services, Inc. + "xn--gk3at1e", // 通販 Amazon Registry Services, Inc. + "xn--hxt814e", // 网店 Zodiac Libra Limited + "xn--i1b6b1a6a2e", // संगठन Public Interest Registry + "xn--imr513n", // 餐厅 HU YI GLOBAL INFORMATION RESOURCES (HOLDING) COMPANY. HONGKONG LIMITED + "xn--io0a7i", // 网络 Computer Network Information Center of Chinese Academy of Sciences (China Internet Network Information Center) + "xn--j1aef", // ком VeriSign Sarl + "xn--jlq61u9w7b", // 诺基亚 Nokia Corporation + "xn--jvr189m", // 食品 Amazon Registry Services, Inc. + "xn--kcrx77d1x4a", // 飞利浦 Koninklijke Philips N.V. + "xn--kpu716f", // 手表 Richemont DNS Inc. + "xn--kput3i", // 手机 Beijing RITT-Net Technology Development Co., Ltd + "xn--mgba3a3ejt", // ارامكو Aramco Services Company + "xn--mgba7c0bbn0a", // العليان Crescent Holding GmbH + "xn--mgbab2bd", // بازار CORE Association + "xn--mgbb9fbpob", // موبايلي GreenTech Consultancy Company W.L.L. + "xn--mgbca7dzdo", // ابوظبي Abu Dhabi Systems and Information Centre + "xn--mgbi4ecexp", // كاثوليك Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) + "xn--mgbt3dhd", // همراه Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. + "xn--mk1bu44c", // 닷컴 VeriSign Sarl + "xn--mxtq1m", // 政府 Net-Chinese Co., Ltd. + "xn--ngbc5azd", // شبكة International Domain Registry Pty. Ltd. + "xn--ngbe9e0a", // بيتك Kuwait Finance House + "xn--nqv7f", // 机构 Public Interest Registry + "xn--nqv7fs00ema", // 组织机构 Public Interest Registry + "xn--nyqy26a", // 健康 Stable Tone Limited + "xn--p1acf", // рус Rusnames Limited + "xn--pbt977c", // 珠宝 Richemont DNS Inc. + "xn--pssy2u", // 大拿 VeriSign Sarl + "xn--q9jyb4c", // みんな Charleston Road Registry Inc. + "xn--qcka1pmc", // グーグル Charleston Road Registry Inc. + "xn--rhqv96g", // 世界 Stable Tone Limited + "xn--rovu88b", // 書籍 Amazon EU S.à r.l. + "xn--ses554g", // 网址 KNET Co., Ltd + "xn--t60b56a", // 닷넷 VeriSign Sarl + "xn--tckwe", // コム VeriSign Sarl + "xn--tiq49xqyj", // 天主教 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) + "xn--unup4y", // 游戏 Spring Fields, LLC + "xn--vermgensberater-ctb", // VERMöGENSBERATER Deutsche Vermögensberatung Aktiengesellschaft DVAG + "xn--vermgensberatung-pwb", // VERMöGENSBERATUNG Deutsche Vermögensberatung Aktiengesellschaft DVAG + "xn--vhquv", // 企业 Dash McCook, LLC + "xn--vuq861b", // 信息 Beijing Tele-info Network Technology Co., Ltd. + "xn--w4r85el8fhu5dnra", // 嘉里大酒店 Kerry Trading Co. Limited + "xn--w4rs40l", // 嘉里 Kerry Trading Co. Limited + "xn--xhq521b", // 广东 Guangzhou YU Wei Information Technology Co., Ltd. + "xn--zfr164b", // 政务 China Organizational Name Administration Center + "xperia", // xperia Sony Mobile Communications AB + "xxx", // xxx ICM Registry LLC + "xyz", // xyz XYZ.COM LLC + "yachts", // yachts DERYachts, LLC + "yahoo", // yahoo Yahoo! Domain Services Inc. + "yamaxun", // yamaxun Amazon Registry Services, Inc. + "yandex", // yandex YANDEX, LLC + "yodobashi", // yodobashi YODOBASHI CAMERA CO.,LTD. + "yoga", // yoga Top Level Domain Holdings Limited + "yokohama", // yokohama GMO Registry, Inc. + "you", // you Amazon Registry Services, Inc. + "youtube", // youtube Charleston Road Registry Inc. + "yun", // yun QIHOO 360 TECHNOLOGY CO. LTD. + "zappos", // zappos Amazon Registry Services, Inc. + "zara", // zara Industria de Diseño Textil, S.A. (INDITEX, S.A.) + "zero", // zero Amazon Registry Services, Inc. + "zip", // zip Charleston Road Registry Inc. + "zippo", // zippo Zadco Company + "zone", // zone Outer Falls, LLC + "zuerich", // zuerich Kanton Zürich (Canton of Zurich) + }; + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static final String[] COUNTRY_CODE_TLDS = new String[] { + "ac", // Ascension Island + "ad", // Andorra + "ae", // United Arab Emirates + "af", // Afghanistan + "ag", // Antigua and Barbuda + "ai", // Anguilla + "al", // Albania + "am", // Armenia +// "an", // Netherlands Antilles (retired) + "ao", // Angola + "aq", // Antarctica + "ar", // Argentina + "as", // American Samoa + "at", // Austria + "au", // Australia (includes Ashmore and Cartier Islands and Coral Sea Islands) + "aw", // Aruba + "ax", // Åland + "az", // Azerbaijan + "ba", // Bosnia and Herzegovina + "bb", // Barbados + "bd", // Bangladesh + "be", // Belgium + "bf", // Burkina Faso + "bg", // Bulgaria + "bh", // Bahrain + "bi", // Burundi + "bj", // Benin + "bm", // Bermuda + "bn", // Brunei Darussalam + "bo", // Bolivia + "br", // Brazil + "bs", // Bahamas + "bt", // Bhutan + "bv", // Bouvet Island + "bw", // Botswana + "by", // Belarus + "bz", // Belize + "ca", // Canada + "cc", // Cocos (Keeling) Islands + "cd", // Democratic Republic of the Congo (formerly Zaire) + "cf", // Central African Republic + "cg", // Republic of the Congo + "ch", // Switzerland + "ci", // Côte d'Ivoire + "ck", // Cook Islands + "cl", // Chile + "cm", // Cameroon + "cn", // China, mainland + "co", // Colombia + "cr", // Costa Rica + "cu", // Cuba + "cv", // Cape Verde + "cw", // Curaçao + "cx", // Christmas Island + "cy", // Cyprus + "cz", // Czech Republic + "de", // Germany + "dj", // Djibouti + "dk", // Denmark + "dm", // Dominica + "do", // Dominican Republic + "dz", // Algeria + "ec", // Ecuador + "ee", // Estonia + "eg", // Egypt + "er", // Eritrea + "es", // Spain + "et", // Ethiopia + "eu", // European Union + "fi", // Finland + "fj", // Fiji + "fk", // Falkland Islands + "fm", // Federated States of Micronesia + "fo", // Faroe Islands + "fr", // France + "ga", // Gabon + "gb", // Great Britain (United Kingdom) + "gd", // Grenada + "ge", // Georgia + "gf", // French Guiana + "gg", // Guernsey + "gh", // Ghana + "gi", // Gibraltar + "gl", // Greenland + "gm", // The Gambia + "gn", // Guinea + "gp", // Guadeloupe + "gq", // Equatorial Guinea + "gr", // Greece + "gs", // South Georgia and the South Sandwich Islands + "gt", // Guatemala + "gu", // Guam + "gw", // Guinea-Bissau + "gy", // Guyana + "hk", // Hong Kong + "hm", // Heard Island and McDonald Islands + "hn", // Honduras + "hr", // Croatia (Hrvatska) + "ht", // Haiti + "hu", // Hungary + "id", // Indonesia + "ie", // Ireland (Éire) + "il", // Israel + "im", // Isle of Man + "in", // India + "io", // British Indian Ocean Territory + "iq", // Iraq + "ir", // Iran + "is", // Iceland + "it", // Italy + "je", // Jersey + "jm", // Jamaica + "jo", // Jordan + "jp", // Japan + "ke", // Kenya + "kg", // Kyrgyzstan + "kh", // Cambodia (Khmer) + "ki", // Kiribati + "km", // Comoros + "kn", // Saint Kitts and Nevis + "kp", // North Korea + "kr", // South Korea + "kw", // Kuwait + "ky", // Cayman Islands + "kz", // Kazakhstan + "la", // Laos (currently being marketed as the official domain for Los Angeles) + "lb", // Lebanon + "lc", // Saint Lucia + "li", // Liechtenstein + "lk", // Sri Lanka + "lr", // Liberia + "ls", // Lesotho + "lt", // Lithuania + "lu", // Luxembourg + "lv", // Latvia + "ly", // Libya + "ma", // Morocco + "mc", // Monaco + "md", // Moldova + "me", // Montenegro + "mg", // Madagascar + "mh", // Marshall Islands + "mk", // Republic of Macedonia + "ml", // Mali + "mm", // Myanmar + "mn", // Mongolia + "mo", // Macau + "mp", // Northern Mariana Islands + "mq", // Martinique + "mr", // Mauritania + "ms", // Montserrat + "mt", // Malta + "mu", // Mauritius + "mv", // Maldives + "mw", // Malawi + "mx", // Mexico + "my", // Malaysia + "mz", // Mozambique + "na", // Namibia + "nc", // New Caledonia + "ne", // Niger + "nf", // Norfolk Island + "ng", // Nigeria + "ni", // Nicaragua + "nl", // Netherlands + "no", // Norway + "np", // Nepal + "nr", // Nauru + "nu", // Niue + "nz", // New Zealand + "om", // Oman + "pa", // Panama + "pe", // Peru + "pf", // French Polynesia With Clipperton Island + "pg", // Papua New Guinea + "ph", // Philippines + "pk", // Pakistan + "pl", // Poland + "pm", // Saint-Pierre and Miquelon + "pn", // Pitcairn Islands + "pr", // Puerto Rico + "ps", // Palestinian territories (PA-controlled West Bank and Gaza Strip) + "pt", // Portugal + "pw", // Palau + "py", // Paraguay + "qa", // Qatar + "re", // Réunion + "ro", // Romania + "rs", // Serbia + "ru", // Russia + "rw", // Rwanda + "sa", // Saudi Arabia + "sb", // Solomon Islands + "sc", // Seychelles + "sd", // Sudan + "se", // Sweden + "sg", // Singapore + "sh", // Saint Helena + "si", // Slovenia + "sj", // Svalbard and Jan Mayen Islands Not in use (Norwegian dependencies; see .no) + "sk", // Slovakia + "sl", // Sierra Leone + "sm", // San Marino + "sn", // Senegal + "so", // Somalia + "sr", // Suriname + "st", // São Tomé and Príncipe + "su", // Soviet Union (deprecated) + "sv", // El Salvador + "sx", // Sint Maarten + "sy", // Syria + "sz", // Swaziland + "tc", // Turks and Caicos Islands + "td", // Chad + "tf", // French Southern and Antarctic Lands + "tg", // Togo + "th", // Thailand + "tj", // Tajikistan + "tk", // Tokelau + "tl", // East Timor (deprecated old code) + "tm", // Turkmenistan + "tn", // Tunisia + "to", // Tonga +// "tp", // East Timor (Retired) + "tr", // Turkey + "tt", // Trinidad and Tobago + "tv", // Tuvalu + "tw", // Taiwan, Republic of China + "tz", // Tanzania + "ua", // Ukraine + "ug", // Uganda + "uk", // United Kingdom + "us", // United States of America + "uy", // Uruguay + "uz", // Uzbekistan + "va", // Vatican City State + "vc", // Saint Vincent and the Grenadines + "ve", // Venezuela + "vg", // British Virgin Islands + "vi", // U.S. Virgin Islands + "vn", // Vietnam + "vu", // Vanuatu + "wf", // Wallis and Futuna + "ws", // Samoa (formerly Western Samoa) + "xn--3e0b707e", // 한국 KISA (Korea Internet & Security Agency) + "xn--45brj9c", // ভারত National Internet Exchange of India + "xn--54b7fta0cc", // বাংলা Posts and Telecommunications Division + "xn--80ao21a", // қаз Association of IT Companies of Kazakhstan + "xn--90a3ac", // срб Serbian National Internet Domain Registry (RNIDS) + "xn--90ais", // ??? Reliable Software Inc. + "xn--clchc0ea0b2g2a9gcd", // சிங்கப்பூர் Singapore Network Information Centre (SGNIC) Pte Ltd + "xn--d1alf", // мкд Macedonian Academic Research Network Skopje + "xn--e1a4c", // ею EURid vzw/asbl + "xn--fiqs8s", // 中国 China Internet Network Information Center + "xn--fiqz9s", // 中國 China Internet Network Information Center + "xn--fpcrj9c3d", // భారత్ National Internet Exchange of India + "xn--fzc2c9e2c", // ලංකා LK Domain Registry + "xn--gecrj9c", // ભારત National Internet Exchange of India + "xn--h2brj9c", // भारत National Internet Exchange of India + "xn--j1amh", // укр Ukrainian Network Information Centre (UANIC), Inc. + "xn--j6w193g", // 香港 Hong Kong Internet Registration Corporation Ltd. + "xn--kprw13d", // 台湾 Taiwan Network Information Center (TWNIC) + "xn--kpry57d", // 台灣 Taiwan Network Information Center (TWNIC) + "xn--l1acc", // мон Datacom Co.,Ltd + "xn--lgbbat1ad8j", // الجزائر CERIST + "xn--mgb9awbf", // عمان Telecommunications Regulatory Authority (TRA) + "xn--mgba3a4f16a", // ایران Institute for Research in Fundamental Sciences (IPM) + "xn--mgbaam7a8h", // امارات Telecommunications Regulatory Authority (TRA) + "xn--mgbayh7gpa", // الاردن National Information Technology Center (NITC) + "xn--mgbbh1a71e", // بھارت National Internet Exchange of India + "xn--mgbc0a9azcg", // المغرب Agence Nationale de Réglementation des Télécommunications (ANRT) + "xn--mgberp4a5d4ar", // السعودية Communications and Information Technology Commission + "xn--mgbpl2fh", // ????? Sudan Internet Society + "xn--mgbtx2b", // عراق Communications and Media Commission (CMC) + "xn--mgbx4cd0ab", // مليسيا MYNIC Berhad + "xn--mix891f", // 澳門 Bureau of Telecommunications Regulation (DSRT) + "xn--node", // გე Information Technologies Development Center (ITDC) + "xn--o3cw4h", // ไทย Thai Network Information Center Foundation + "xn--ogbpf8fl", // سورية National Agency for Network Services (NANS) + "xn--p1ai", // рф Coordination Center for TLD RU + "xn--pgbs0dh", // تونس Agence Tunisienne d'Internet + "xn--qxam", // ελ ICS-FORTH GR + "xn--s9brj9c", // ਭਾਰਤ National Internet Exchange of India + "xn--wgbh1c", // مصر National Telecommunication Regulatory Authority - NTRA + "xn--wgbl6a", // قطر Communications Regulatory Authority + "xn--xkc2al3hye2a", // இலங்கை LK Domain Registry + "xn--xkc2dl3a5ee0h", // இந்தியா National Internet Exchange of India + "xn--y9a3aq", // ??? Internet Society + "xn--yfro4i67o", // 新加坡 Singapore Network Information Centre (SGNIC) Pte Ltd + "xn--ygbi2ammx", // فلسطين Ministry of Telecom & Information Technology (MTIT) + "ye", // Yemen + "yt", // Mayotte + "za", // South Africa + "zm", // Zambia + "zw", // Zimbabwe + }; + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static final String[] LOCAL_TLDS = new String[] { + "localdomain", // Also widely used as localhost.localdomain + "localhost", // RFC2606 defined + }; + + // Additional arrays to supplement or override the built in ones. + // The PLUS arrays are valid keys, the MINUS arrays are invalid keys + + /* + * This field is used to detect whether the getInstance has been called. + * After this, the method updateTLDOverride is not allowed to be called. + * This field does not need to be volatile since it is only accessed from + * synchronized methods. + */ + private static boolean inUse = false; + + /* + * These arrays are mutable, but they don't need to be volatile. + * They can only be updated by the updateTLDOverride method, and any readers must get an instance + * using the getInstance methods which are all (now) synchronised. + */ + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static volatile String[] countryCodeTLDsPlus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static volatile String[] genericTLDsPlus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static volatile String[] countryCodeTLDsMinus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + + // WARNING: this array MUST be sorted, otherwise it cannot be searched reliably using binary search + private static volatile String[] genericTLDsMinus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + + /** + * enum used by {@link DomainValidator#updateTLDOverride(DomainValidator.ArrayType, String[])} + * to determine which override array to update / fetch + * @since 1.5.0 + * @since 1.5.1 made public and added read-only array references + */ + public enum ArrayType { + /** Update (or get a copy of) the GENERIC_TLDS_PLUS table containing additonal generic TLDs */ + GENERIC_PLUS, + /** Update (or get a copy of) the GENERIC_TLDS_MINUS table containing deleted generic TLDs */ + GENERIC_MINUS, + /** Update (or get a copy of) the COUNTRY_CODE_TLDS_PLUS table containing additonal country code TLDs */ + COUNTRY_CODE_PLUS, + /** Update (or get a copy of) the COUNTRY_CODE_TLDS_MINUS table containing deleted country code TLDs */ + COUNTRY_CODE_MINUS, + /** Get a copy of the generic TLDS table */ + GENERIC_RO, + /** Get a copy of the country code table */ + COUNTRY_CODE_RO, + /** Get a copy of the infrastructure table */ + INFRASTRUCTURE_RO, + /** Get a copy of the local table */ + LOCAL_RO + ; + }; + + // For use by unit test code only + static synchronized void clearTLDOverrides() { + inUse = false; + countryCodeTLDsPlus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + countryCodeTLDsMinus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + genericTLDsPlus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + genericTLDsMinus = MemoryReductionUtil.EMPTY_STRING_ARRAY; + } + /** + * Update one of the TLD override arrays. + * This must only be done at program startup, before any instances are accessed using getInstance. + *

+ * For example: + *

+ * {@code DomainValidator.updateTLDOverride(ArrayType.GENERIC_PLUS, new String[]{"apache"})} + *

+ * To clear an override array, provide an empty array. + * + * @param table the table to update, see {@link DomainValidator.ArrayType} + * Must be one of the following + *

    + *
  • COUNTRY_CODE_MINUS
  • + *
  • COUNTRY_CODE_PLUS
  • + *
  • GENERIC_MINUS
  • + *
  • GENERIC_PLUS
  • + *
+ * @param tlds the array of TLDs, must not be null + * @throws IllegalStateException if the method is called after getInstance + * @throws IllegalArgumentException if one of the read-only tables is requested + * @since 1.5.0 + */ + public static synchronized void updateTLDOverride(DomainValidator.ArrayType table, String [] tlds) { + if (inUse) { + throw new IllegalStateException("Can only invoke this method before calling getInstance"); + } + String [] copy = new String[tlds.length]; + // Comparisons are always done with lower-case entries + for (int i = 0; i < tlds.length; i++) { + copy[i] = tlds[i].toLowerCase(Locale.ENGLISH); + } + Arrays.sort(copy); + switch(table) { + case COUNTRY_CODE_MINUS: + countryCodeTLDsMinus = copy; + break; + case COUNTRY_CODE_PLUS: + countryCodeTLDsPlus = copy; + break; + case GENERIC_MINUS: + genericTLDsMinus = copy; + break; + case GENERIC_PLUS: + genericTLDsPlus = copy; + break; + case COUNTRY_CODE_RO: + case GENERIC_RO: + case INFRASTRUCTURE_RO: + case LOCAL_RO: + throw new IllegalArgumentException("Cannot update the table: " + table); + default: + throw new IllegalArgumentException("Unexpected enum value: " + table); + } + } + + /** + * Get a copy of the internal array. + * @param table the array type (any of the enum values) + * @return a copy of the array + * @throws IllegalArgumentException if the table type is unexpected (should not happen) + * @since 1.5.1 + */ + public static String [] getTLDEntries(DomainValidator.ArrayType table) { + final String array[]; + switch(table) { + case COUNTRY_CODE_MINUS: + array = countryCodeTLDsMinus; + break; + case COUNTRY_CODE_PLUS: + array = countryCodeTLDsPlus; + break; + case GENERIC_MINUS: + array = genericTLDsMinus; + break; + case GENERIC_PLUS: + array = genericTLDsPlus; + break; + case GENERIC_RO: + array = GENERIC_TLDS; + break; + case COUNTRY_CODE_RO: + array = COUNTRY_CODE_TLDS; + break; + case INFRASTRUCTURE_RO: + array = INFRASTRUCTURE_TLDS; + break; + case LOCAL_RO: + array = LOCAL_TLDS; + break; + default: + throw new IllegalArgumentException("Unexpected enum value: " + table); + } + return Arrays.copyOf(array, array.length); // clone the array + } + + /** + * Converts potentially Unicode input to punycode. + * If conversion fails, returns the original input. + * + * @param input the string to convert, not null + * @return converted input, or original input if conversion fails + */ + // Needed by UrlValidator + //[PATCH] + public + // end of [PATCH] + static String unicodeToASCII(String input) { + if (isOnlyASCII(input)) { // skip possibly expensive processing + return input; + } + try { + final String ascii = IDN.toASCII(input); + if (DomainValidator.IDNBUGHOLDER.IDN_TOASCII_PRESERVES_TRAILING_DOTS) { + return ascii; + } + final int length = input.length(); + if (length == 0) {// check there is a last character + return input; + } + // RFC3490 3.1. 1) + // Whenever dots are used as label separators, the following + // characters MUST be recognized as dots: U+002E (full stop), U+3002 + // (ideographic full stop), U+FF0E (fullwidth full stop), U+FF61 + // (halfwidth ideographic full stop). + char lastChar = input.charAt(length-1);// fetch original last char + switch(lastChar) { + case '\u002E': // "." full stop + case '\u3002': // ideographic full stop + case '\uFF0E': // fullwidth full stop + case '\uFF61': // halfwidth ideographic full stop + return ascii + "."; // restore the missing stop + default: + return ascii; + } + } catch (IllegalArgumentException e) { // input is not valid + return input; + } + } + + private static class IDNBUGHOLDER { + private static boolean keepsTrailingDot() { + final String input = "a."; // must be a valid name + return input.equals(IDN.toASCII(input)); + } + private static final boolean IDN_TOASCII_PRESERVES_TRAILING_DOTS = keepsTrailingDot(); + } + + /* + * Check if input contains only ASCII + * Treats null as all ASCII + */ + private static boolean isOnlyASCII(String input) { + if (input == null) { + return true; + } + for(int i=0; i < input.length(); i++) { + if (input.charAt(i) > 0x7F) { // CHECKSTYLE IGNORE MagicNumber + return false; + } + } + return true; + } + + /** + * Check if a sorted array contains the specified key + * + * @param sortedArray the array to search + * @param key the key to find + * @return {@code true} if the array contains the key + */ + private static boolean arrayContains(String[] sortedArray, String key) { + return Arrays.binarySearch(sortedArray, key) >= 0; + } +} diff --git a/core/src/main/java/jenkins/org/apache/commons/validator/routines/InetAddressValidator.java b/core/src/main/java/jenkins/org/apache/commons/validator/routines/InetAddressValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..59ac7ceb2b0ad55cf00e3a01ae4e5bb4633a0625 --- /dev/null +++ b/core/src/main/java/jenkins/org/apache/commons/validator/routines/InetAddressValidator.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Copied from commons-validator:commons-validator:1.6, with [PATCH] modifications */ +package jenkins.org.apache.commons.validator.routines; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + *

InetAddress validation and conversion routines (java.net.InetAddress).

+ * + *

This class provides methods to validate a candidate IP address. + * + *

+ * This class is a Singleton; you can retrieve the instance via the {@link #getInstance()} method. + *

+ * + * @version $Revision: 1783032 $ + * @since Validator 1.4 + */ +//[PATCH] +@Restricted(NoExternalUse.class) +// end of [PATCH] +public class InetAddressValidator implements Serializable { + + private static final int IPV4_MAX_OCTET_VALUE = 255; + + private static final int MAX_UNSIGNED_SHORT = 0xffff; + + private static final int BASE_16 = 16; + + private static final long serialVersionUID = -919201640201914789L; + + private static final String IPV4_REGEX = + "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$"; + + // Max number of hex groups (separated by :) in an IPV6 address + private static final int IPV6_MAX_HEX_GROUPS = 8; + + // Max hex digits in each IPv6 group + private static final int IPV6_MAX_HEX_DIGITS_PER_GROUP = 4; + + /** + * Singleton instance of this class. + */ + private static final InetAddressValidator VALIDATOR = new InetAddressValidator(); + + /** IPv4 RegexValidator */ + private final RegexValidator ipv4Validator = new RegexValidator(IPV4_REGEX); + + /** + * Returns the singleton instance of this validator. + * @return the singleton instance of this validator + */ + public static InetAddressValidator getInstance() { + return VALIDATOR; + } + + /** + * Checks if the specified string is a valid IP address. + * @param inetAddress the string to validate + * @return true if the string validates as an IP address + */ + public boolean isValid(String inetAddress) { + return isValidInet4Address(inetAddress) || isValidInet6Address(inetAddress); + } + + /** + * Validates an IPv4 address. Returns true if valid. + * @param inet4Address the IPv4 address to validate + * @return true if the argument contains a valid IPv4 address + */ + public boolean isValidInet4Address(String inet4Address) { + // verify that address conforms to generic IPv4 format + String[] groups = ipv4Validator.match(inet4Address); + + if (groups == null) { + return false; + } + + // verify that address subgroups are legal + for (String ipSegment : groups) { + if (ipSegment == null || ipSegment.length() == 0) { + return false; + } + + int iIpSegment = 0; + + try { + iIpSegment = Integer.parseInt(ipSegment); + } catch(NumberFormatException e) { + return false; + } + + if (iIpSegment > IPV4_MAX_OCTET_VALUE) { + return false; + } + + if (ipSegment.length() > 1 && ipSegment.startsWith("0")) { + return false; + } + + } + + return true; + } + + /** + * Validates an IPv6 address. Returns true if valid. + * @param inet6Address the IPv6 address to validate + * @return true if the argument contains a valid IPv6 address + * + * @since 1.4.1 + */ + public boolean isValidInet6Address(String inet6Address) { + boolean containsCompressedZeroes = inet6Address.contains("::"); + if (containsCompressedZeroes && (inet6Address.indexOf("::") != inet6Address.lastIndexOf("::"))) { + return false; + } + if ((inet6Address.startsWith(":") && !inet6Address.startsWith("::")) + || (inet6Address.endsWith(":") && !inet6Address.endsWith("::"))) { + return false; + } + String[] octets = inet6Address.split(":"); + if (containsCompressedZeroes) { + List octetList = new ArrayList(Arrays.asList(octets)); + if (inet6Address.endsWith("::")) { + // String.split() drops ending empty segments + octetList.add(""); + } else if (inet6Address.startsWith("::") && !octetList.isEmpty()) { + octetList.remove(0); + } + octets = octetList.toArray(new String[octetList.size()]); + } + if (octets.length > IPV6_MAX_HEX_GROUPS) { + return false; + } + int validOctets = 0; + int emptyOctets = 0; // consecutive empty chunks + for (int index = 0; index < octets.length; index++) { + String octet = octets[index]; + if (octet.length() == 0) { + emptyOctets++; + if (emptyOctets > 1) { + return false; + } + } else { + emptyOctets = 0; + // Is last chunk an IPv4 address? + if (index == octets.length - 1 && octet.contains(".")) { + if (!isValidInet4Address(octet)) { + return false; + } + validOctets += 2; + continue; + } + if (octet.length() > IPV6_MAX_HEX_DIGITS_PER_GROUP) { + return false; + } + int octetInt = 0; + try { + octetInt = Integer.parseInt(octet, BASE_16); + } catch (NumberFormatException e) { + return false; + } + if (octetInt < 0 || octetInt > MAX_UNSIGNED_SHORT) { + return false; + } + } + validOctets++; + } + if (validOctets > IPV6_MAX_HEX_GROUPS || (validOctets < IPV6_MAX_HEX_GROUPS && !containsCompressedZeroes)) { + return false; + } + return true; + } +} diff --git a/core/src/main/java/jenkins/org/apache/commons/validator/routines/RegexValidator.java b/core/src/main/java/jenkins/org/apache/commons/validator/routines/RegexValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..eebc3747d9ab4f85a0896b1da1d240d1f1068456 --- /dev/null +++ b/core/src/main/java/jenkins/org/apache/commons/validator/routines/RegexValidator.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Copied from commons-validator:commons-validator:1.6, with [PATCH] modifications */ +package jenkins.org.apache.commons.validator.routines; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Regular Expression validation (using JDK 1.4+ regex support). + *

+ * Construct the validator either for a single regular expression or a set (array) of + * regular expressions. By default validation is case sensitive but constructors + * are provided to allow case in-sensitive validation. For example to create + * a validator which does case in-sensitive validation for a set of regular + * expressions: + *

+ *
+ * 
+ * String[] regexs = new String[] {...};
+ * RegexValidator validator = new RegexValidator(regexs, false);
+ * 
+ * 
+ * + *
    + *
  • Validate true or false:
  • + *
  • + *
      + *
    • boolean valid = validator.isValidRootUrl(value);
    • + *
    + *
  • + *
  • Validate returning an aggregated String of the matched groups:
  • + *
  • + *
      + *
    • String result = validator.validate(value);
    • + *
    + *
  • + *
  • Validate returning the matched groups:
  • + *
  • + *
      + *
    • String[] result = validator.match(value);
    • + *
    + *
  • + *
+ * + * Note that patterns are matched against the entire input. + * + *

+ * Cached instances pre-compile and re-use {@link Pattern}(s) - which according + * to the {@link Pattern} API are safe to use in a multi-threaded environment. + *

+ * + * @version $Revision: 1739356 $ + * @since Validator 1.4 + */ +//[PATCH] +@Restricted(NoExternalUse.class) +// end of [PATCH] +public class RegexValidator implements Serializable { + + private static final long serialVersionUID = -8832409930574867162L; + + private final Pattern[] patterns; + + /** + * Construct a case sensitive validator for a single + * regular expression. + * + * @param regex The regular expression this validator will + * validate against + */ + public RegexValidator(String regex) { + this(regex, true); + } + + /** + * Construct a validator for a single regular expression + * with the specified case sensitivity. + * + * @param regex The regular expression this validator will + * validate against + * @param caseSensitive when true matching is case + * sensitive, otherwise matching is case in-sensitive + */ + public RegexValidator(String regex, boolean caseSensitive) { + this(new String[] {regex}, caseSensitive); + } + + /** + * Construct a case sensitive validator that matches any one + * of the set of regular expressions. + * + * @param regexs The set of regular expressions this validator will + * validate against + */ + public RegexValidator(String[] regexs) { + this(regexs, true); + } + + /** + * Construct a validator that matches any one of the set of regular + * expressions with the specified case sensitivity. + * + * @param regexs The set of regular expressions this validator will + * validate against + * @param caseSensitive when true matching is case + * sensitive, otherwise matching is case in-sensitive + */ + public RegexValidator(String[] regexs, boolean caseSensitive) { + if (regexs == null || regexs.length == 0) { + throw new IllegalArgumentException("Regular expressions are missing"); + } + patterns = new Pattern[regexs.length]; + int flags = (caseSensitive ? 0: Pattern.CASE_INSENSITIVE); + for (int i = 0; i < regexs.length; i++) { + if (regexs[i] == null || regexs[i].length() == 0) { + throw new IllegalArgumentException("Regular expression[" + i + "] is missing"); + } + patterns[i] = Pattern.compile(regexs[i], flags); + } + } + + /** + * Validate a value against the set of regular expressions. + * + * @param value The value to validate. + * @return true if the value is valid + * otherwise false. + */ + public boolean isValid(String value) { + if (value == null) { + return false; + } + for (int i = 0; i < patterns.length; i++) { + if (patterns[i].matcher(value).matches()) { + return true; + } + } + return false; + } + + /** + * Validate a value against the set of regular expressions + * returning the array of matched groups. + * + * @param value The value to validate. + * @return String array of the groups matched if + * valid or null if invalid + */ + public String[] match(String value) { + if (value == null) { + return null; + } + for (int i = 0; i < patterns.length; i++) { + Matcher matcher = patterns[i].matcher(value); + if (matcher.matches()) { + int count = matcher.groupCount(); + String[] groups = new String[count]; + for (int j = 0; j < count; j++) { + groups[j] = matcher.group(j+1); + } + return groups; + } + } + return null; + } + + + /** + * Validate a value against the set of regular expressions + * returning a String value of the aggregated groups. + * + * @param value The value to validate. + * @return Aggregated String value comprised of the + * groups matched if valid or null if invalid + */ + public String validate(String value) { + if (value == null) { + return null; + } + for (int i = 0; i < patterns.length; i++) { + Matcher matcher = patterns[i].matcher(value); + if (matcher.matches()) { + int count = matcher.groupCount(); + if (count == 1) { + return matcher.group(1); + } + StringBuilder buffer = new StringBuilder(); + for (int j = 0; j < count; j++) { + String component = matcher.group(j+1); + if (component != null) { + buffer.append(component); + } + } + return buffer.toString(); + } + } + return null; + } + + /** + * Provide a String representation of this validator. + * @return A String representation of this validator + */ + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append("RegexValidator{"); + for (int i = 0; i < patterns.length; i++) { + if (i > 0) { + buffer.append(","); + } + buffer.append(patterns[i].pattern()); + } + buffer.append("}"); + return buffer.toString(); + } + +} diff --git a/core/src/main/java/jenkins/org/apache/commons/validator/routines/UrlValidator.java b/core/src/main/java/jenkins/org/apache/commons/validator/routines/UrlValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..f309b177e63653a46b491c15cbefa44ba432900c --- /dev/null +++ b/core/src/main/java/jenkins/org/apache/commons/validator/routines/UrlValidator.java @@ -0,0 +1,551 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Copied from commons-validator:commons-validator:1.6, with [PATCH] modifications */ +package jenkins.org.apache.commons.validator.routines; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

URL Validation routines.

+ * Behavior of validation is modified by passing in options: + *
    + *
  • ALLOW_2_SLASHES - [FALSE] Allows double '/' characters in the path + * component.
  • + *
  • NO_FRAGMENT- [FALSE] By default fragments are allowed, if this option is + * included then fragments are flagged as illegal.
  • + *
  • ALLOW_ALL_SCHEMES - [FALSE] By default only http, https, and ftp are + * considered valid schemes. Enabling this option will let any scheme pass validation.
  • + *
+ * + *

Originally based in on php script by Debbie Dyer, validation.php v1.2b, Date: 03/07/02, + * http://javascript.internet.com. However, this validation now bears little resemblance + * to the php original.

+ *
+ *   Example of usage:
+ *   Construct a UrlValidator with valid schemes of "http", and "https".
+ *
+ *    String[] schemes = {"http","https"}.
+ *    UrlValidator urlValidator = new UrlValidator(schemes);
+ *    if (urlValidator.isValidRootUrl("ftp://foo.bar.com/")) {
+ *       System.out.println("url is valid");
+ *    } else {
+ *       System.out.println("url is invalid");
+ *    }
+ *
+ *    prints "url is invalid"
+ *   If instead the default constructor is used.
+ *
+ *    UrlValidator urlValidator = new UrlValidator();
+ *    if (urlValidator.isValidRootUrl("ftp://foo.bar.com/")) {
+ *       System.out.println("url is valid");
+ *    } else {
+ *       System.out.println("url is invalid");
+ *    }
+ *
+ *   prints out "url is valid"
+ *  
+ * + * @see + * + * Uniform Resource Identifiers (URI): Generic Syntax + * + * + * @version $Revision: 1783203 $ + * @since Validator 1.4 + */ +//[PATCH] +@Restricted(NoExternalUse.class) +// end of [PATCH] +public class UrlValidator implements Serializable { + + private static final long serialVersionUID = 7557161713937335013L; + + private static final int MAX_UNSIGNED_16_BIT_INT = 0xFFFF; // port max + + /** + * Allows all validly formatted schemes to pass validation instead of + * supplying a set of valid schemes. + */ + public static final long ALLOW_ALL_SCHEMES = 1 << 0; + + /** + * Allow two slashes in the path component of the URL. + */ + public static final long ALLOW_2_SLASHES = 1 << 1; + + /** + * Enabling this options disallows any URL fragments. + */ + public static final long NO_FRAGMENTS = 1 << 2; + + /** + * Allow local URLs, such as http://localhost/ or http://machine/ . + * This enables a broad-brush check, for complex local machine name + * validation requirements you should create your validator with + * a {@link RegexValidator} instead ({@link #UrlValidator(RegexValidator, long)}) + */ + public static final long ALLOW_LOCAL_URLS = 1 << 3; // CHECKSTYLE IGNORE MagicNumber + + /** + * This expression derived/taken from the BNF for URI (RFC2396). + */ + private static final String URL_REGEX = + "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"; + // 12 3 4 5 6 7 8 9 + private static final Pattern URL_PATTERN = Pattern.compile(URL_REGEX); + + /** + * Schema/Protocol (ie. http:, ftp:, file:, etc). + */ + private static final int PARSE_URL_SCHEME = 2; + + /** + * Includes hostname/ip and port number. + */ + private static final int PARSE_URL_AUTHORITY = 4; + + private static final int PARSE_URL_PATH = 5; + + private static final int PARSE_URL_QUERY = 7; + + private static final int PARSE_URL_FRAGMENT = 9; + + /** + * Protocol scheme (e.g. http, ftp, https). + */ + private static final String SCHEME_REGEX = "^\\p{Alpha}[\\p{Alnum}\\+\\-\\.]*"; + private static final Pattern SCHEME_PATTERN = Pattern.compile(SCHEME_REGEX); + + // Drop numeric, and "+-." for now + // TODO does not allow for optional userinfo. + // Validation of character set is done by isValidAuthority + private static final String AUTHORITY_CHARS_REGEX = "\\p{Alnum}\\-\\."; // allows for IPV4 but not IPV6 + private static final String IPV6_REGEX = "[0-9a-fA-F:]+"; // do this as separate match because : could cause ambiguity with port prefix + + // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + // We assume that password has the same valid chars as user info + private static final String USERINFO_CHARS_REGEX = "[a-zA-Z0-9%-._~!$&'()*+,;=]"; + // since neither ':' nor '@' are allowed chars, we don't need to use non-greedy matching + private static final String USERINFO_FIELD_REGEX = + USERINFO_CHARS_REGEX + "+" + // At least one character for the name + "(?::" + USERINFO_CHARS_REGEX + "*)?@"; // colon and password may be absent + private static final String AUTHORITY_REGEX = + "(?:\\[("+IPV6_REGEX+")\\]|(?:(?:"+USERINFO_FIELD_REGEX+")?([" + AUTHORITY_CHARS_REGEX + "]*)))(?::(\\d*))?(.*)?"; + // 1 e.g. user:pass@ 2 3 4 + private static final Pattern AUTHORITY_PATTERN = Pattern.compile(AUTHORITY_REGEX); + + private static final int PARSE_AUTHORITY_IPV6 = 1; + + private static final int PARSE_AUTHORITY_HOST_IP = 2; // excludes userinfo, if present + + private static final int PARSE_AUTHORITY_PORT = 3; // excludes leading colon + + /** + * Should always be empty. The code currently allows spaces. + */ + private static final int PARSE_AUTHORITY_EXTRA = 4; + + private static final String PATH_REGEX = "^(/[-\\w:@&?=+,.!/~*'%$_;\\(\\)]*)?$"; + private static final Pattern PATH_PATTERN = Pattern.compile(PATH_REGEX); + + private static final String QUERY_REGEX = "^(\\S*)$"; + private static final Pattern QUERY_PATTERN = Pattern.compile(QUERY_REGEX); + + /** + * Holds the set of current validation options. + */ + private final long options; + + /** + * The set of schemes that are allowed to be in a URL. + */ + private final Set allowedSchemes; // Must be lower-case + + /** + * Regular expressions used to manually validate authorities if IANA + * domain name validation isn't desired. + */ + private final RegexValidator authorityValidator; + + /** + * If no schemes are provided, default to this set. + */ + private static final String[] DEFAULT_SCHEMES = {"http", "https", "ftp"}; // Must be lower-case + + /** + * Singleton instance of this class with default schemes and options. + */ + private static final UrlValidator DEFAULT_URL_VALIDATOR = new UrlValidator(); + + /** + * Returns the singleton instance of this class with default schemes and options. + * @return singleton instance with default schemes and options + */ + public static UrlValidator getInstance() { + return DEFAULT_URL_VALIDATOR; + } + + /** + * Create a UrlValidator with default properties. + */ + public UrlValidator() { + this(null); + } + + /** + * Behavior of validation is modified by passing in several strings options: + * @param schemes Pass in one or more url schemes to consider valid, passing in + * a null will default to "http,https,ftp" being valid. + * If a non-null schemes is specified then all valid schemes must + * be specified. Setting the ALLOW_ALL_SCHEMES option will + * ignore the contents of schemes. + */ + public UrlValidator(String[] schemes) { + this(schemes, 0L); + } + + /** + * Initialize a UrlValidator with the given validation options. + * @param options The options should be set using the public constants declared in + * this class. To set multiple options you simply add them together. For example, + * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options. + */ + public UrlValidator(long options) { + this(null, null, options); + } + + /** + * Behavior of validation is modified by passing in options: + * @param schemes The set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set. + * @param options The options should be set using the public constants declared in + * this class. To set multiple options you simply add them together. For example, + * ALLOW_2_SLASHES + NO_FRAGMENTS enables both of those options. + */ + public UrlValidator(String[] schemes, long options) { + this(schemes, null, options); + } + + /** + * Initialize a UrlValidator with the given validation options. + * @param authorityValidator Regular expression validator used to validate the authority part + * This allows the user to override the standard set of domains. + * @param options Validation options. Set using the public constants of this class. + * To set multiple options, simply add them together: + *

ALLOW_2_SLASHES + NO_FRAGMENTS

+ * enables both of those options. + */ + public UrlValidator(RegexValidator authorityValidator, long options) { + this(null, authorityValidator, options); + } + + /** + * Customizable constructor. Validation behavior is modifed by passing in options. + * @param schemes the set of valid schemes. Ignored if the ALLOW_ALL_SCHEMES option is set. + * @param authorityValidator Regular expression validator used to validate the authority part + * @param options Validation options. Set using the public constants of this class. + * To set multiple options, simply add them together: + *

ALLOW_2_SLASHES + NO_FRAGMENTS

+ * enables both of those options. + */ + public UrlValidator(String[] schemes, RegexValidator authorityValidator, long options) { + this.options = options; + + if (isOn(ALLOW_ALL_SCHEMES)) { + allowedSchemes = Collections.emptySet(); + } else { + if (schemes == null) { + schemes = DEFAULT_SCHEMES; + } + allowedSchemes = new HashSet(schemes.length); + for(int i=0; i < schemes.length; i++) { + allowedSchemes.add(schemes[i].toLowerCase(Locale.ENGLISH)); + } + } + + this.authorityValidator = authorityValidator; + } + + /** + *

Checks if a field has a valid url address.

+ * + * Note that the method calls #isValidAuthority() + * which checks that the domain is valid. + * + * @param value The value validation is being performed on. A null + * value is considered invalid. + * @return true if the url is valid. + */ + public boolean isValid(String value) { + if (value == null) { + return false; + } + + // Check the whole url address structure + Matcher urlMatcher = URL_PATTERN.matcher(value); + if (!urlMatcher.matches()) { + return false; + } + + String scheme = urlMatcher.group(PARSE_URL_SCHEME); + if (!isValidScheme(scheme)) { + return false; + } + + String authority = urlMatcher.group(PARSE_URL_AUTHORITY); + if ("file".equals(scheme)) {// Special case - file: allows an empty authority + if (authority != null) { + if (authority.contains(":")) { // but cannot allow trailing : + return false; + } + } + // drop through to continue validation + } else { // not file: + // Validate the authority + if (!isValidAuthority(authority)) { + return false; + } + } + + if (!isValidPath(urlMatcher.group(PARSE_URL_PATH))) { + return false; + } + + if (!isValidQuery(urlMatcher.group(PARSE_URL_QUERY))) { + return false; + } + + if (!isValidFragment(urlMatcher.group(PARSE_URL_FRAGMENT))) { + return false; + } + + return true; + } + + /** + * Validate scheme. If schemes[] was initialized to a non null, + * then only those schemes are allowed. + * Otherwise the default schemes are "http", "https", "ftp". + * Matching is case-blind. + * @param scheme The scheme to validate. A null value is considered + * invalid. + * @return true if valid. + */ + protected boolean isValidScheme(String scheme) { + if (scheme == null) { + return false; + } + + // TODO could be removed if external schemes were checked in the ctor before being stored + if (!SCHEME_PATTERN.matcher(scheme).matches()) { + return false; + } + + if (isOff(ALLOW_ALL_SCHEMES) && !allowedSchemes.contains(scheme.toLowerCase(Locale.ENGLISH))) { + return false; + } + + return true; + } + + /** + * Returns true if the authority is properly formatted. An authority is the combination + * of hostname and port. A null authority value is considered invalid. + * Note: this implementation validates the domain unless a RegexValidator was provided. + * If a RegexValidator was supplied and it matches, then the authority is regarded + * as valid with no further checks, otherwise the method checks against the + * AUTHORITY_PATTERN and the DomainValidator (ALLOW_LOCAL_URLS) + * @param authority Authority value to validate, alllows IDN + * @return true if authority (hostname and port) is valid. + */ + protected boolean isValidAuthority(String authority) { + if (authority == null) { + return false; + } + + // check manual authority validation if specified + if (authorityValidator != null && authorityValidator.isValid(authority)) { + return true; + } + // convert to ASCII if possible + final String authorityASCII = DomainValidator.unicodeToASCII(authority); + + Matcher authorityMatcher = AUTHORITY_PATTERN.matcher(authorityASCII); + if (!authorityMatcher.matches()) { + return false; + } + + // We have to process IPV6 separately because that is parsed in a different group + String ipv6 = authorityMatcher.group(PARSE_AUTHORITY_IPV6); + if (ipv6 != null) { + InetAddressValidator inetAddressValidator = InetAddressValidator.getInstance(); + if (!inetAddressValidator.isValidInet6Address(ipv6)) { + return false; + } + } else { + String hostLocation = authorityMatcher.group(PARSE_AUTHORITY_HOST_IP); + // check if authority is hostname or IP address: + // try a hostname first since that's much more likely + DomainValidator domainValidator = DomainValidator.getInstance(isOn(ALLOW_LOCAL_URLS)); + if (!domainValidator.isValid(hostLocation)) { + // try an IPv4 address + InetAddressValidator inetAddressValidator = InetAddressValidator.getInstance(); + if (!inetAddressValidator.isValidInet4Address(hostLocation)) { + // isn't IPv4, so the URL is invalid + return false; + } + } + String port = authorityMatcher.group(PARSE_AUTHORITY_PORT); + if (port != null && port.length() > 0) { + try { + int iPort = Integer.parseInt(port); + if (iPort < 0 || iPort > MAX_UNSIGNED_16_BIT_INT) { + return false; + } + } catch (NumberFormatException nfe) { + return false; // this can happen for big numbers + } + } + } + + String extra = authorityMatcher.group(PARSE_AUTHORITY_EXTRA); + if (extra != null && extra.trim().length() > 0){ + return false; + } + + return true; + } + + /** + * Returns true if the path is valid. A null value is considered invalid. + * @param path Path value to validate. + * @return true if path is valid. + */ + protected boolean isValidPath(String path) { + if (path == null) { + return false; + } + + if (!PATH_PATTERN.matcher(path).matches()) { + return false; + } + + try { + URI uri = new URI(null,null,path,null); + String norm = uri.normalize().getPath(); + if (norm.startsWith("/../") // Trying to go via the parent dir + || norm.equals("/..")) { // Trying to go to the parent dir + return false; + } + } catch (URISyntaxException e) { + return false; + } + + int slash2Count = countToken("//", path); + if (isOff(ALLOW_2_SLASHES) && (slash2Count > 0)) { + return false; + } + + return true; + } + + /** + * Returns true if the query is null or it's a properly formatted query string. + * @param query Query value to validate. + * @return true if query is valid. + */ + protected boolean isValidQuery(String query) { + if (query == null) { + return true; + } + + return QUERY_PATTERN.matcher(query).matches(); + } + + /** + * Returns true if the given fragment is null or fragments are allowed. + * @param fragment Fragment value to validate. + * @return true if fragment is valid. + */ + protected boolean isValidFragment(String fragment) { + if (fragment == null) { + return true; + } + + return isOff(NO_FRAGMENTS); + } + + /** + * Returns the number of times the token appears in the target. + * @param token Token value to be counted. + * @param target Target value to count tokens in. + * @return the number of tokens. + */ + protected int countToken(String token, String target) { + int tokenIndex = 0; + int count = 0; + while (tokenIndex != -1) { + tokenIndex = target.indexOf(token, tokenIndex); + if (tokenIndex > -1) { + tokenIndex++; + count++; + } + } + return count; + } + + /** + * Tests whether the given flag is on. If the flag is not a power of 2 + * (ie. 3) this tests whether the combination of flags is on. + * + * @param flag Flag value to check. + * + * @return whether the specified flag value is on. + */ + private boolean isOn(long flag) { + return (options & flag) > 0; + } + + /** + * Tests whether the given flag is off. If the flag is not a power of 2 + * (ie. 3) this tests whether the combination of flags is off. + * + * @param flag Flag value to check. + * + * @return whether the specified flag value is off. + */ + private boolean isOff(long flag) { + return (options & flag) == 0; + } + + // Unit test access to pattern matcher + Matcher matchURL(String value) { + return URL_PATTERN.matcher(value); + } +} diff --git a/core/src/main/java/jenkins/scm/RunWithSCM.java b/core/src/main/java/jenkins/scm/RunWithSCM.java index 551716503cf98d30034c19aac7273be5dc8081e8..1523b5a2ad8a63258bb3bacf0c1d8302b778b67a 100644 --- a/core/src/main/java/jenkins/scm/RunWithSCM.java +++ b/core/src/main/java/jenkins/scm/RunWithSCM.java @@ -48,7 +48,7 @@ import java.util.logging.Logger; /** * Allows a {@link Run} to provide {@link SCM}-related methods, such as providing changesets and culprits. * - * @since FIXME + * @since 2.60 */ public interface RunWithSCM, RunT extends Run & RunWithSCM> { @@ -84,6 +84,9 @@ public interface RunWithSCM, * This list at least always include people who made changes in this build, but * if the previous build was a failure it also includes the culprit list from there. * + *

+ * Missing {@link User}s will be created on-demand. + * * @return * can be empty but never null. */ @@ -99,7 +102,8 @@ public interface RunWithSCM, public Iterator iterator() { return new AdaptedIterator(culpritIds.iterator()) { protected User adapt(String id) { - return User.get(id); + // TODO: Probably it should not auto-create users + return User.getById(id, true); } }; } diff --git a/core/src/main/java/hudson/slaves/CommandConnector.java b/core/src/main/java/jenkins/security/ApiCrumbExclusion.java similarity index 57% rename from core/src/main/java/hudson/slaves/CommandConnector.java rename to core/src/main/java/jenkins/security/ApiCrumbExclusion.java index 327b3ba9dcb2d555ce7ceafff8b851d393af0690..8eea5cee135167e9333467188c6174298ba7aa51 100644 --- a/core/src/main/java/hudson/slaves/CommandConnector.java +++ b/core/src/main/java/jenkins/security/ApiCrumbExclusion.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2010, InfraDNA, Inc. + * Copyright (c) 2017 CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,39 +21,33 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson.slaves; +package jenkins.security; -import hudson.EnvVars; import hudson.Extension; -import hudson.model.TaskListener; +import hudson.security.csrf.CrumbExclusion; import org.jenkinsci.Symbol; -import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** - * Executes a program on the master and expect that script to connect. - * - * @author Kohsuke Kawaguchi + * JENKINS-22474: Makes API Token calls bypass CSRF protection to ease usage */ -public class CommandConnector extends ComputerConnector { - public final String command; - - @DataBoundConstructor - public CommandConnector(String command) { - this.command = command; - } - +@Symbol("apiToken") +@Extension +@Restricted(DoNotUse.class) +public class ApiCrumbExclusion extends CrumbExclusion { @Override - public CommandLauncher launch(String host, TaskListener listener) throws IOException, InterruptedException { - return new CommandLauncher(command,new EnvVars("SLAVE",host)); - } - - @Extension @Symbol("command") - public static class DescriptorImpl extends ComputerConnectorDescriptor { - @Override - public String getDisplayName() { - return Messages.CommandLauncher_displayName(); + public boolean process(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + if (Boolean.TRUE.equals(request.getAttribute(BasicHeaderApiTokenAuthenticator.class.getName()))) { + chain.doFilter(request, response); + return true; } + return false; } } diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java index dd64d0e9d05862d842375c8c4ac4cc2e8fccd83b..8e61e0350a55a06e09bae3ff02f42c34126eb52b 100644 --- a/core/src/main/java/jenkins/security/ApiTokenProperty.java +++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java @@ -23,9 +23,13 @@ */ package jenkins.security; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; -import jenkins.util.SystemProperties; import hudson.Util; +import jenkins.security.apitoken.ApiTokenPropertyConfiguration; +import jenkins.security.apitoken.ApiTokenStats; +import jenkins.security.apitoken.ApiTokenStore; +import jenkins.util.SystemProperties; import hudson.model.Descriptor.FormException; import hudson.model.User; import hudson.model.UserProperty; @@ -34,19 +38,33 @@ import hudson.security.ACL; import hudson.util.HttpResponses; import hudson.util.Secret; import jenkins.model.Jenkins; +import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import java.io.IOException; -import java.nio.charset.Charset; -import java.security.MessageDigest; import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; + import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -61,49 +79,108 @@ import org.kohsuke.stapler.interceptor.RequirePOST; * @since 1.426 */ public class ApiTokenProperty extends UserProperty { - private volatile Secret apiToken; + private static final Logger LOGGER = Logger.getLogger(ApiTokenProperty.class.getName()); /** - * If enabled, shows API tokens to users with {@link Jenkins#ADMINISTER) permissions. - * Disabled by default due to the security reasons. + * If enabled, the users with {@link Jenkins#ADMINISTER} permissions can view legacy tokens for + * other users.

+ * Disabled by default due to the security reasons.

* If enabled, it restores the original Jenkins behavior (SECURITY-200). + * * @since 1.638 */ - private static final boolean SHOW_TOKEN_TO_ADMINS = + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts") + private static /* not final */ boolean SHOW_LEGACY_TOKEN_TO_ADMINS = SystemProperties.getBoolean(ApiTokenProperty.class.getName() + ".showTokenToAdmins"); + /** + * If enabled, the users with {@link Jenkins#ADMINISTER} permissions can generate new tokens for + * other users. Normally a user can only generate tokens for himself.

+ * Take care that only the creator of a token will have the plain value as it's only stored as an hash in the system.

+ * Disabled by default due to the security reasons. + * It's the version of {@link #SHOW_LEGACY_TOKEN_TO_ADMINS} for the new API Token system (SECURITY-200). + * + * @since 2.129 + */ + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts") + private static /* not final */ boolean ADMIN_CAN_GENERATE_NEW_TOKENS = + SystemProperties.getBoolean(ApiTokenProperty.class.getName() + ".adminCanGenerateNewTokens"); + + private volatile Secret apiToken; + private ApiTokenStore tokenStore; + + /** + * Store the usage information of the different token for this user + * The save operation can be toggled by using {@link ApiTokenPropertyConfiguration#usageStatisticsEnabled} + * The information are stored in a separate file to avoid problem with some configuration synchronization tools + */ + private transient ApiTokenStats tokenStats; @DataBoundConstructor public ApiTokenProperty() { - _changeApiToken(); } - + + @Override + protected void setUser(User u) { + super.setUser(u); + + if (this.tokenStore == null) { + this.tokenStore = new ApiTokenStore(); + } + if(this.tokenStats == null){ + this.tokenStats = ApiTokenStats.load(user); + } + if(this.apiToken != null){ + this.tokenStore.regenerateTokenFromLegacyIfRequired(this.apiToken); + } + } + /** * We don't let the external code set the API token, * but for the initial value of the token we need to compute the seed by ourselves. */ - /*package*/ ApiTokenProperty(String seed) { - apiToken = Secret.fromString(seed); + /*package*/ ApiTokenProperty(@CheckForNull String seed) { + if(seed != null){ + apiToken = Secret.fromString(seed); + } } /** * Gets the API token. * The method performs security checks since 1.638. Only the current user and SYSTEM may see it. - * Users with {@link Jenkins#ADMINISTER} may be allowed to do it using {@link #SHOW_TOKEN_TO_ADMINS}. + * Users with {@link Jenkins#ADMINISTER} may be allowed to do it using {@link #SHOW_LEGACY_TOKEN_TO_ADMINS}. * * @return API Token. Never null, but may be {@link Messages#ApiTokenProperty_ChangeToken_TokenIsHidden()} * if the user has no appropriate permissions. * @since 1.426, and since 1.638 the method performs security checks */ @Nonnull + @SuppressFBWarnings("NP_NONNULL_RETURN_VIOLATION") public String getApiToken() { - return hasPermissionToSeeToken() ? getApiTokenInsecure() + LOGGER.log(Level.FINE, "Deprecated usage of getApiToken"); + if(LOGGER.isLoggable(Level.FINER)){ + LOGGER.log(Level.FINER, "Deprecated usage of getApiToken (trace)", new Exception()); + } + return hasPermissionToSeeToken() + ? getApiTokenInsecure() : Messages.ApiTokenProperty_ChangeToken_TokenIsHidden(); } + /** + * Determine if the legacy token is still present + */ + @Restricted(NoExternalUse.class) + public boolean hasLegacyToken(){ + return apiToken != null; + } + @Nonnull @Restricted(NoExternalUse.class) /*package*/ String getApiTokenInsecure() { + if(apiToken == null){ + return Messages.ApiTokenProperty_NoLegacyToken(); + } + String p = apiToken.getPlainText(); if (p.equals(Util.getDigestOf(Jenkins.getInstance().getSecretKey()+":"+user.getId()))) { // if the current token is the initial value created by pre SECURITY-49 Jenkins, we can't use that. @@ -112,24 +189,32 @@ public class ApiTokenProperty extends UserProperty { } return Util.getDigestOf(p); } - - public boolean matchesPassword(String password) { - String token = getApiTokenInsecure(); - // String.equals isn't constant time, but this is - return MessageDigest.isEqual(password.getBytes(Charset.forName("US-ASCII")), - token.getBytes(Charset.forName("US-ASCII"))); + + public boolean matchesPassword(String token) { + if(StringUtils.isBlank(token)){ + return false; + } + + ApiTokenStore.HashedToken matchingToken = tokenStore.findMatchingToken(token); + if(matchingToken == null){ + return false; + } + + tokenStats.updateUsageForId(matchingToken.getUuid()); + + return true; } + /** + * Only for legacy token + */ private boolean hasPermissionToSeeToken() { - final Jenkins jenkins = Jenkins.getInstance(); - // Administrators can do whatever they want - if (SHOW_TOKEN_TO_ADMINS && jenkins.hasPermission(Jenkins.ADMINISTER)) { + if (SHOW_LEGACY_TOKEN_TO_ADMINS && Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return true; } - - final User current = User.current(); + User current = User.current(); if (current == null) { // Anonymous return false; } @@ -138,36 +223,158 @@ public class ApiTokenProperty extends UserProperty { if (Jenkins.getAuthentication() == ACL.SYSTEM) { return true; } - - //TODO: replace by IdStrategy in newer Jenkins versions - //return User.idStrategy().equals(user.getId(), current.getId()); - return StringUtils.equals(user.getId(), current.getId()); + + return User.idStrategy().equals(user.getId(), current.getId()); + } + + // only for Jelly + @Restricted(NoExternalUse.class) + public Collection getTokenList() { + return tokenStore.getTokenListSortedByName() + .stream() + .map(token -> { + ApiTokenStats.SingleTokenStats stats = tokenStats.findTokenStatsById(token.getUuid()); + return new TokenInfoAndStats(token, stats); + }) + .collect(Collectors.toList()); + } + + // only for Jelly + @Immutable + @Restricted(NoExternalUse.class) + public static class TokenInfoAndStats { + public final String uuid; + public final String name; + public final Date creationDate; + public final long numDaysCreation; + public final boolean isLegacy; + + public final int useCounter; + public final Date lastUseDate; + public final long numDaysUse; + + public TokenInfoAndStats(@Nonnull ApiTokenStore.HashedToken token, @Nonnull ApiTokenStats.SingleTokenStats stats) { + this.uuid = token.getUuid(); + this.name = token.getName(); + this.creationDate = token.getCreationDate(); + this.numDaysCreation = token.getNumDaysCreation(); + this.isLegacy = token.isLegacy(); + + this.useCounter = stats.getUseCounter(); + this.lastUseDate = stats.getLastUseDate(); + this.numDaysUse = stats.getNumDaysUse(); + } } + + /** + * Allow user to rename tokens + */ + @Override + public UserProperty reconfigure(StaplerRequest req, @CheckForNull JSONObject form) throws FormException { + if(form == null){ + return this; + } + Object tokenStoreData = form.get("tokenStore"); + Map tokenStoreTypedData = convertToTokenMap(tokenStoreData); + this.tokenStore.reconfigure(tokenStoreTypedData); + return this; + } + + private Map convertToTokenMap(Object tokenStoreData) { + if (tokenStoreData == null) { + // in case there are no token + return Collections.emptyMap(); + } else if (tokenStoreData instanceof JSONObject) { + // in case there is only one token + JSONObject singleTokenData = (JSONObject) tokenStoreData; + Map result = new HashMap<>(); + addJSONTokenIntoMap(result, singleTokenData); + return result; + } else if (tokenStoreData instanceof JSONArray) { + // in case there are multiple tokens + JSONArray tokenArray = ((JSONArray) tokenStoreData); + Map result = new HashMap<>(); + for (int i = 0; i < tokenArray.size(); i++) { + JSONObject tokenData = tokenArray.getJSONObject(i); + addJSONTokenIntoMap(result, tokenData); + } + return result; + } + + throw HttpResponses.error(400, "Unexpected class received for the token store information"); + } + + private void addJSONTokenIntoMap(Map tokenMap, JSONObject tokenData) { + String uuid = tokenData.getString("tokenUuid"); + tokenMap.put(uuid, tokenData); + } + + /** + * Only usable if the user still has the legacy API token. + * @deprecated Each token can be revoked now and new tokens can be requested without altering existing ones. + */ + @Deprecated public void changeApiToken() throws IOException { + // just to keep the same level of security user.checkPermission(Jenkins.ADMINISTER); + + LOGGER.log(Level.FINE, "Deprecated usage of changeApiToken"); + + ApiTokenStore.HashedToken existingLegacyToken = tokenStore.getLegacyToken(); _changeApiToken(); + tokenStore.regenerateTokenFromLegacy(apiToken); + + if(existingLegacyToken != null){ + tokenStats.removeId(existingLegacyToken.getUuid()); + } user.save(); } - - private void _changeApiToken() { + + @Deprecated + private void _changeApiToken(){ byte[] random = new byte[16]; // 16x8=128bit worth of randomness, since we use md5 digest as the API token RANDOM.nextBytes(random); apiToken = Secret.fromString(Util.toHexString(random)); } - - @Override - public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException { - return this; + + /** + * Does not revoke the token stored in the store + */ + @Restricted(NoExternalUse.class) + public void deleteApiToken(){ + this.apiToken = null; + } + + @Restricted(NoExternalUse.class) + public ApiTokenStore getTokenStore() { + return tokenStore; + } + + @Restricted(NoExternalUse.class) + public ApiTokenStats getTokenStats() { + return tokenStats; } - @Extension @Symbol("apiToken") + @Extension + @Symbol("apiToken") public static final class DescriptorImpl extends UserPropertyDescriptor { public String getDisplayName() { return Messages.ApiTokenProperty_DisplayName(); } + @Restricted(NoExternalUse.class) // Jelly use + public String getNoLegacyToken(){ + return Messages.ApiTokenProperty_NoLegacyToken(); + } + /** + * New approach: + * API Token are generated only when a user request a new one. The value is randomly generated + * without any link to the user and only displayed to him the first time. + * We only store the hash for future comparisons. + * + * Legacy approach: * When we are creating a default {@link ApiTokenProperty} for User, * we need to make sure it yields the same value for the same user, * because there's no guarantee that the property is saved. @@ -176,29 +383,190 @@ public class ApiTokenProperty extends UserProperty { * the initial API token value. So we take the seed by hashing the secret + user ID. */ public ApiTokenProperty newInstance(User user) { - return new ApiTokenProperty(API_KEY_SEED.mac(user.getId())); + if (!ApiTokenPropertyConfiguration.get().isTokenGenerationOnCreationEnabled()) { + return forceNewInstance(user, false); + } + + return forceNewInstance(user, true); + } + + private ApiTokenProperty forceNewInstance(User user, boolean withLegacyToken) { + if(withLegacyToken){ + return new ApiTokenProperty(API_KEY_SEED.mac(user.getId())); + }else{ + return new ApiTokenProperty(null); + } + } + + // for Jelly view + @Restricted(NoExternalUse.class) + public boolean isStatisticsEnabled(){ + return ApiTokenPropertyConfiguration.get().isUsageStatisticsEnabled(); + } + + // for Jelly view + @Restricted(NoExternalUse.class) + public boolean mustDisplayLegacyApiToken(User propertyOwner) { + ApiTokenProperty property = propertyOwner.getProperty(ApiTokenProperty.class); + if(property != null && property.apiToken != null){ + return true; + } + return ApiTokenPropertyConfiguration.get().isCreationOfLegacyTokenEnabled(); + } + + // for Jelly view + @Restricted(NoExternalUse.class) + public boolean hasCurrentUserRightToGenerateNewToken(User propertyOwner){ + if (ADMIN_CAN_GENERATE_NEW_TOKENS && Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + return true; + } + + User currentUser = User.current(); + if (currentUser == null) { + // Anonymous + return false; + } + + if (Jenkins.getAuthentication() == ACL.SYSTEM) { + // SYSTEM user is always eligible to see tokens + return true; + } + + return User.idStrategy().equals(propertyOwner.getId(), currentUser.getId()); } + /** + * @deprecated use {@link #doGenerateNewToken(User, String)} instead + */ + @Deprecated @RequirePOST public HttpResponse doChangeToken(@AncestorInPath User u, StaplerResponse rsp) throws IOException { + // you are the user or you have ADMINISTER permission + u.checkPermission(Jenkins.ADMINISTER); + + LOGGER.log(Level.FINE, "Deprecated action /changeToken used, consider using /generateNewToken instead"); + + if(!mustDisplayLegacyApiToken(u)){ + // user does not have legacy token and the capability to create one without an existing one is disabled + return HttpResponses.html(Messages.ApiTokenProperty_ChangeToken_CapabilityNotAllowed()); + } + ApiTokenProperty p = u.getProperty(ApiTokenProperty.class); - if (p==null) { - p = newInstance(u); + if (p == null) { + p = forceNewInstance(u, true); + p.setUser(u); u.addProperty(p); } else { + // even if the user does not have legacy token, this method let some legacy system to regenerate one p.changeApiToken(); } + rsp.setHeader("script","document.getElementById('apiToken').value='"+p.getApiToken()+"'"); - return HttpResponses.html(p.hasPermissionToSeeToken() - ? Messages.ApiTokenProperty_ChangeToken_Success() + return HttpResponses.html(p.hasPermissionToSeeToken() + ? Messages.ApiTokenProperty_ChangeToken_Success() : Messages.ApiTokenProperty_ChangeToken_SuccessHidden()); } - } + @RequirePOST + public HttpResponse doGenerateNewToken(@AncestorInPath User u, @QueryParameter String newTokenName) throws IOException { + if(!hasCurrentUserRightToGenerateNewToken(u)){ + return HttpResponses.forbidden(); + } + + final String tokenName; + if (StringUtils.isBlank(newTokenName)) { + tokenName = String.format("Token created on %s", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now())); + }else{ + tokenName = newTokenName; + } + + ApiTokenProperty p = u.getProperty(ApiTokenProperty.class); + if (p == null) { + p = forceNewInstance(u, false); + u.addProperty(p); + } + + ApiTokenStore.TokenUuidAndPlainValue tokenUuidAndPlainValue = p.tokenStore.generateNewToken(tokenName); + u.save(); + + return HttpResponses.okJSON(new HashMap() {{ + put("tokenUuid", tokenUuidAndPlainValue.tokenUuid); + put("tokenName", tokenName); + put("tokenValue", tokenUuidAndPlainValue.plainValue); + }}); + } + + @RequirePOST + public HttpResponse doRename(@AncestorInPath User u, + @QueryParameter String tokenUuid, @QueryParameter String newName) throws IOException { + // only current user + administrator can rename token + u.checkPermission(Jenkins.ADMINISTER); + + if (StringUtils.isBlank(newName)) { + return HttpResponses.errorJSON("The name cannot be empty"); + } + if(StringUtils.isBlank(tokenUuid)){ + // using the web UI this should not occur + return HttpResponses.errorWithoutStack(400, "The tokenUuid cannot be empty"); + } + + ApiTokenProperty p = u.getProperty(ApiTokenProperty.class); + if (p == null) { + return HttpResponses.errorWithoutStack(400, "The user does not have any ApiToken yet, try generating one before."); + } + + boolean renameOk = p.tokenStore.renameToken(tokenUuid, newName); + if(!renameOk){ + // that could potentially happen if the token is removed from another page + // between your page loaded and your action + return HttpResponses.errorJSON("No token found, try refreshing the page"); + } + + u.save(); + + return HttpResponses.ok(); + } + + @RequirePOST + public HttpResponse doRevoke(@AncestorInPath User u, + @QueryParameter String tokenUuid) throws IOException { + // only current user + administrator can revoke token + u.checkPermission(Jenkins.ADMINISTER); + + if(StringUtils.isBlank(tokenUuid)){ + // using the web UI this should not occur + return HttpResponses.errorWithoutStack(400, "The tokenUuid cannot be empty"); + } + + ApiTokenProperty p = u.getProperty(ApiTokenProperty.class); + if (p == null) { + return HttpResponses.errorWithoutStack(400, "The user does not have any ApiToken yet, try generating one before."); + } + + ApiTokenStore.HashedToken revoked = p.tokenStore.revokeToken(tokenUuid); + if(revoked != null){ + if(revoked.isLegacy()){ + // if the user revoked the API Token, we can delete it + p.apiToken = null; + } + p.tokenStats.removeId(revoked.getUuid()); + } + u.save(); + + return HttpResponses.ok(); + } + } + + /** + * Only used for legacy API Token generation and change. After that token is revoked, it will be useless. + */ + @Deprecated private static final SecureRandom RANDOM = new SecureRandom(); /** * We don't want an API key that's too long, so cut the length to 16 (which produces 32-letter MAC code in hexdump) */ - private static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey(ApiTokenProperty.class,"seed",16); + @Deprecated + @Restricted(NoExternalUse.class) + public static final HMACConfidentialKey API_KEY_SEED = new HMACConfidentialKey(ApiTokenProperty.class,"seed",16); } diff --git a/core/src/main/java/jenkins/security/BasicApiTokenHelper.java b/core/src/main/java/jenkins/security/BasicApiTokenHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..ed45727b438273529174ac4632437dacf20f8fb0 --- /dev/null +++ b/core/src/main/java/jenkins/security/BasicApiTokenHelper.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security; + +import hudson.Util; +import hudson.model.User; +import jenkins.model.GlobalConfiguration; +import jenkins.security.apitoken.ApiTokenPropertyConfiguration; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +@Restricted(NoExternalUse.class) +public class BasicApiTokenHelper { + public static @CheckForNull User isConnectingUsingApiToken(String username, String tokenValue){ + User user = User.getById(username, false); + if(user == null){ + ApiTokenPropertyConfiguration apiTokenConfiguration = GlobalConfiguration.all().getInstance(ApiTokenPropertyConfiguration.class); + if(apiTokenConfiguration.isTokenGenerationOnCreationEnabled()){ + String generatedTokenOnCreation = Util.getDigestOf(ApiTokenProperty.API_KEY_SEED.mac(username)); + boolean areTokenEqual = MessageDigest.isEqual( + generatedTokenOnCreation.getBytes(StandardCharsets.US_ASCII), + tokenValue.getBytes(StandardCharsets.US_ASCII) + ); + if(areTokenEqual){ + // directly return the user freshly created + // and no need to check its token as the generated token + // will be the same as the one we checked just above + return User.getById(username, true); + } + } + }else{ + ApiTokenProperty t = user.getProperty(ApiTokenProperty.class); + if (t!=null && t.matchesPassword(tokenValue)) { + return user; + } + } + + return null; + } +} diff --git a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java index a192dddc5fc3abff2c05705aab255344ca2f6ce2..2b0fb730ba2e7dbccc793d56574e8fd765b0f20f 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java +++ b/core/src/main/java/jenkins/security/BasicHeaderApiTokenAuthenticator.java @@ -3,6 +3,7 @@ package jenkins.security; import hudson.Extension; import hudson.model.User; import org.acegisecurity.Authentication; +import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.springframework.dao.DataAccessException; @@ -21,22 +22,31 @@ import static java.util.logging.Level.*; */ @Extension public class BasicHeaderApiTokenAuthenticator extends BasicHeaderAuthenticator { + /** + * Note: if the token does not exist or does not match, we do not use {@link SecurityListener#fireFailedToAuthenticate(String)} + * because it will be done in the {@link BasicHeaderRealPasswordAuthenticator} in the case the password is not valid either + */ @Override public Authentication authenticate(HttpServletRequest req, HttpServletResponse rsp, String username, String password) throws ServletException { - // attempt to authenticate as API token - User u = User.getById(username, true); - ApiTokenProperty t = u.getProperty(ApiTokenProperty.class); - if (t!=null && t.matchesPassword(password)) { + User u = BasicApiTokenHelper.isConnectingUsingApiToken(username, password); + if(u != null) { + Authentication auth; try { - return u.impersonate(); + UserDetails userDetails = u.getUserDetailsForImpersonation(); + auth = u.impersonate(userDetails); + + SecurityListener.fireAuthenticated(userDetails); } catch (UsernameNotFoundException x) { // The token was valid, but the impersonation failed. This token is clearly not his real password, // so there's no point in continuing the request processing. Report this error and abort. - LOGGER.log(WARNING, "API token matched for user "+username+" but the impersonation failed",x); + LOGGER.log(WARNING, "API token matched for user " + username + " but the impersonation failed", x); throw new ServletException(x); } catch (DataAccessException x) { throw new ServletException(x); } + + req.setAttribute(BasicHeaderApiTokenAuthenticator.class.getName(), true); + return auth; } return null; } diff --git a/core/src/main/java/jenkins/security/BasicHeaderProcessor.java b/core/src/main/java/jenkins/security/BasicHeaderProcessor.java index 243e044acb441043b8ba5063c4e9c852e73aa8a2..5e0986eca4b5afc1ee08e99d54d1b96a193124fe 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderProcessor.java +++ b/core/src/main/java/jenkins/security/BasicHeaderProcessor.java @@ -12,6 +12,7 @@ import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.ui.AuthenticationEntryPoint; import org.acegisecurity.ui.rememberme.NullRememberMeServices; import org.acegisecurity.ui.rememberme.RememberMeServices; +import org.apache.commons.lang.StringUtils; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -28,7 +29,7 @@ import java.util.logging.Logger; import static java.util.logging.Level.*; /** - * Takes "username:password" given in the Authorization HTTP header and authenticates + * Takes "username:password" given in the {@code Authorization} HTTP header and authenticates * the request. * *

@@ -60,7 +61,7 @@ public class BasicHeaderProcessor implements Filter { HttpServletResponse rsp = (HttpServletResponse) response; String authorization = req.getHeader("Authorization"); - if (authorization!=null && authorization.startsWith("Basic ")) { + if (StringUtils.startsWithIgnoreCase(authorization,"Basic ")) { // authenticate the user String uidpassword = Scrambler.descramble(authorization.substring(6)); int idx = uidpassword.indexOf(':'); diff --git a/core/src/main/java/jenkins/security/ChannelConfigurator.java b/core/src/main/java/jenkins/security/ChannelConfigurator.java index fd7c26e229e6f6fe44fe8f6a9ee8d4fb14b37c9d..91aba888466012675c2fa347dfdb54711a0d0cf3 100644 --- a/core/src/main/java/jenkins/security/ChannelConfigurator.java +++ b/core/src/main/java/jenkins/security/ChannelConfigurator.java @@ -13,7 +13,7 @@ import javax.annotation.Nullable; * Intercepts the new creation of {@link Channel} and tweak its configuration. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public abstract class ChannelConfigurator implements ExtensionPoint { /** diff --git a/core/src/main/java/jenkins/security/ClassFilterImpl.java b/core/src/main/java/jenkins/security/ClassFilterImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..98e2073c017c8b7fd8885971c4e01a50b6c28e9f --- /dev/null +++ b/core/src/main/java/jenkins/security/ClassFilterImpl.java @@ -0,0 +1,341 @@ +/* + * The MIT License + * + * Copyright 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import hudson.ExtensionList; +import hudson.Main; +import hudson.remoting.ClassFilter; +import hudson.remoting.Which; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.CodeSource; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import org.apache.commons.io.IOUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Customized version of {@link ClassFilter#DEFAULT}. + * First of all, {@link CustomClassFilter}s are given the first right of decision. + * Then delegates to {@link ClassFilter#STANDARD} for its blacklist. + * A class not mentioned in the blacklist is permitted unless it is defined in some third-party library + * (as opposed to {@code jenkins-core.jar}, a plugin JAR, or test code during {@link Main#isUnitTest}) + * yet is not mentioned in {@code whitelisted-classes.txt}. + */ +@Restricted(NoExternalUse.class) +public class ClassFilterImpl extends ClassFilter { + + private static final Logger LOGGER = Logger.getLogger(ClassFilterImpl.class.getName()); + + private static /* not final */ boolean SUPPRESS_WHITELIST = SystemProperties.getBoolean("jenkins.security.ClassFilterImpl.SUPPRESS_WHITELIST"); + private static /* not final */ boolean SUPPRESS_ALL = SystemProperties.getBoolean("jenkins.security.ClassFilterImpl.SUPPRESS_ALL"); + + private static final String JENKINS_LOC = codeSource(Jenkins.class); + private static final String REMOTING_LOC = codeSource(ClassFilter.class); + + /** + * Register this implementation as the default in the system. + */ + public static void register() { + if (Main.isUnitTest && JENKINS_LOC == null) { + mockOff(); + return; + } + ClassFilter.setDefault(new ClassFilterImpl()); + if (SUPPRESS_ALL) { + LOGGER.warning("All class filtering suppressed. Your Jenkins installation is at risk from known attacks. See https://jenkins.io/redirect/class-filter/"); + } else if (SUPPRESS_WHITELIST) { + LOGGER.warning("JEP-200 class filtering by whitelist suppressed. Your Jenkins installation may be at risk. See https://jenkins.io/redirect/class-filter/"); + } + } + + /** + * Undo {@link #register}. + */ + public static void unregister() { + ClassFilter.setDefault(ClassFilter.STANDARD); + } + + private static void mockOff() { + LOGGER.warning("Disabling class filtering since we appear to be in a special test environment, perhaps Mockito/PowerMock"); + ClassFilter.setDefault(ClassFilter.NONE); // even Method on the standard blacklist is going to explode + } + + @VisibleForTesting + /*package*/ ClassFilterImpl() {} + + /** Whether a given class is blacklisted. */ + private final Map, Boolean> cache = Collections.synchronizedMap(new WeakHashMap<>()); + /** Whether a given code source location is whitelisted. */ + private final Map codeSourceCache = Collections.synchronizedMap(new HashMap<>()); + /** Names of classes outside Jenkins core or plugins which have a special serial form but are considered safe. */ + static final Set WHITELISTED_CLASSES; + static { + try (InputStream is = ClassFilterImpl.class.getResourceAsStream("whitelisted-classes.txt")) { + WHITELISTED_CLASSES = ImmutableSet.copyOf(IOUtils.readLines(is, StandardCharsets.UTF_8).stream().filter(line -> !line.matches("#.*|\\s*")).collect(Collectors.toSet())); + } catch (IOException x) { + throw new ExceptionInInitializerError(x); + } + } + + @SuppressWarnings("rawtypes") + @Override + public boolean isBlacklisted(Class _c) { + for (CustomClassFilter f : ExtensionList.lookup(CustomClassFilter.class)) { + Boolean r = f.permits(_c); + if (r != null) { + if (r) { + LOGGER.log(Level.FINER, "{0} specifies a policy for {1}: {2}", new Object[] {f, _c.getName(), true}); + } else { + notifyRejected(_c, _c.getName(), String.format("%s specifies a policy for %s: %s ", f, _c.getName(), r)); + } + return !r; + } + } + return cache.computeIfAbsent(_c, c -> { + String name = c.getName(); + if (Main.isUnitTest && (name.contains("$$EnhancerByMockitoWithCGLIB$$") || name.contains("$$FastClassByMockitoWithCGLIB$$") || name.startsWith("org.mockito."))) { + mockOff(); + return false; + } + if (ClassFilter.STANDARD.isBlacklisted(c)) { // currently never true, but may issue diagnostics + notifyRejected(_c, _c.getName(), String.format("%s is not permitted ", _c.getName())); + return true; + } + if (c.isArray()) { + LOGGER.log(Level.FINE, "permitting {0} since it is an array", name); + return false; + } + if (Throwable.class.isAssignableFrom(c)) { + LOGGER.log(Level.FINE, "permitting {0} since it is a throwable", name); + return false; + } + if (Enum.class.isAssignableFrom(c)) { // Class.isEnum seems to be false for, e.g., java.util.concurrent.TimeUnit$6 + LOGGER.log(Level.FINE, "permitting {0} since it is an enum", name); + return false; + } + String location = codeSource(c); + if (location != null) { + if (isLocationWhitelisted(location)) { + LOGGER.log(Level.FINE, "permitting {0} due to its location in {1}", new Object[] {name, location}); + return false; + } + } else { + ClassLoader loader = c.getClassLoader(); + if (loader != null && loader.getClass().getName().equals("hudson.remoting.RemoteClassLoader")) { + LOGGER.log(Level.FINE, "permitting {0} since it was loaded by a remote class loader", name); + return false; + } + } + if (WHITELISTED_CLASSES.contains(name)) { + LOGGER.log(Level.FINE, "tolerating {0} by whitelist", name); + return false; + } + if (SUPPRESS_WHITELIST || SUPPRESS_ALL) { + notifyRejected(_c, null, + String.format("%s in %s might be dangerous, so would normally be rejected; see https://jenkins.io/redirect/class-filter/", name, location != null ?location : "JRE")); + + return false; + } + notifyRejected(_c, null, + String.format("%s in %s might be dangerous, so rejecting; see https://jenkins.io/redirect/class-filter/", name, location != null ?location : "JRE")); + return true; + }); + } + + private static final Pattern CLASSES_JAR = Pattern.compile("(file:/.+/)WEB-INF/lib/classes[.]jar"); + private boolean isLocationWhitelisted(String _loc) { + return codeSourceCache.computeIfAbsent(_loc, loc -> { + if (loc.equals(JENKINS_LOC)) { + LOGGER.log(Level.FINE, "{0} seems to be the location of Jenkins core, OK", loc); + return true; + } + if (loc.equals(REMOTING_LOC)) { + LOGGER.log(Level.FINE, "{0} seems to be the location of Remoting, OK", loc); + return true; + } + if (loc.matches("file:/.+[.]jar")) { + try (JarFile jf = new JarFile(new File(new URI(loc)), false)) { + Manifest mf = jf.getManifest(); + if (mf != null) { + if (isPluginManifest(mf)) { + LOGGER.log(Level.FINE, "{0} seems to be a Jenkins plugin, OK", loc); + return true; + } else { + LOGGER.log(Level.FINE, "{0} does not look like a Jenkins plugin", loc); + } + } else { + LOGGER.log(Level.FINE, "ignoring {0} with no manifest", loc); + } + } catch (Exception x) { + LOGGER.log(Level.WARNING, "problem checking " + loc, x); + } + } + Matcher m = CLASSES_JAR.matcher(loc); + if (m.matches()) { + // Cf. ClassicPluginStrategy.createClassJarFromWebInfClasses: handle legacy plugin format with unpacked WEB-INF/classes/ + try { + File manifestFile = new File(new URI(m.group(1) + "META-INF/MANIFEST.MF")); + if (manifestFile.isFile()) { + try (InputStream is = new FileInputStream(manifestFile)) { + if (isPluginManifest(new Manifest(is))) { + LOGGER.log(Level.FINE, "{0} looks like a Jenkins plugin based on {1}, OK", new Object[] {loc, manifestFile}); + return true; + } else { + LOGGER.log(Level.FINE, "{0} does not look like a Jenkins plugin", manifestFile); + } + } + } else { + LOGGER.log(Level.FINE, "{0} has no matching {1}", new Object[] {loc, manifestFile}); + } + } catch (Exception x) { + LOGGER.log(Level.WARNING, "problem checking " + loc, x); + } + } + if (loc.endsWith("/target/classes/") || loc.matches(".+/build/classes/[^/]+/main/")) { + LOGGER.log(Level.FINE, "{0} seems to be current plugin classes, OK", loc); + return true; + } + if (Main.isUnitTest) { + if (loc.endsWith("/target/test-classes/") || loc.endsWith("-tests.jar") || loc.matches(".+/build/classes/[^/]+/test/")) { + LOGGER.log(Level.FINE, "{0} seems to be test classes, OK", loc); + return true; + } + if (loc.matches(".+/jenkins-test-harness-.+[.]jar")) { + LOGGER.log(Level.FINE, "{0} seems to be jenkins-test-harness, OK", loc); + return true; + } + } + LOGGER.log(Level.FINE, "{0} is not recognized; rejecting", loc); + return false; + }); + } + + /** + * Tries to determine what JAR file a given class was loaded from. + * The location is an opaque string suitable only for comparison to others. + * Similar to {@link Which#jarFile(Class)} but potentially faster, and more tolerant of unknown URL formats. + * @param c some class + * @return something typically like {@code file:/…/plugins/structs/WEB-INF/lib/structs-1.10.jar}; + * or null for classes in the Java Platform, some generated classes, etc. + */ + private static @CheckForNull String codeSource(@Nonnull Class c) { + CodeSource cs = c.getProtectionDomain().getCodeSource(); + if (cs == null) { + return null; + } + URL loc = cs.getLocation(); + if (loc == null) { + return null; + } + String r = loc.toString(); + if (r.endsWith(".class")) { + // JENKINS-49147: Tomcat bug. Now do the more expensive check… + String suffix = c.getName().replace('.', '/') + ".class"; + if (r.endsWith(suffix)) { + r = r.substring(0, r.length() - suffix.length()); + } + } + if (r.startsWith("jar:file:/") && r.endsWith(".jar!/")) { + // JENKINS-49543: also an old behavior of Tomcat. Legal enough, but unexpected by isLocationWhitelisted. + r = r.substring(4, r.length() - 2); + } + return r; + } + + private static boolean isPluginManifest(Manifest mf) { + Attributes attr = mf.getMainAttributes(); + return attr.getValue("Short-Name") != null && (attr.getValue("Plugin-Version") != null || attr.getValue("Jenkins-Version") != null) || + "true".equals(attr.getValue("Jenkins-ClassFilter-Whitelisted")); + } + + @Override + public boolean isBlacklisted(String name) { + if (Main.isUnitTest && name.contains("$$EnhancerByMockitoWithCGLIB$$")) { + mockOff(); + return false; + } + for (CustomClassFilter f : ExtensionList.lookup(CustomClassFilter.class)) { + Boolean r = f.permits(name); + if (r != null) { + if (r) { + LOGGER.log(Level.FINER, "{0} specifies a policy for {1}: {2}", new Object[] {f, name, true}); + } else { + notifyRejected(null, name, + String.format("%s specifies a policy for %s: %s", f, name, r)); + } + + return !r; + } + } + // could apply a cache if the pattern search turns out to be slow + if (ClassFilter.STANDARD.isBlacklisted(name)) { + if (SUPPRESS_ALL) { + notifyRejected(null, name, + String.format("would normally reject %s according to standard blacklist; see https://jenkins.io/redirect/class-filter/", name)); + return false; + } + notifyRejected(null, name, + String.format("rejecting %s according to standard blacklist; see https://jenkins.io/redirect/class-filter/", name)); + return true; + } else { + return false; + } + } + + private void notifyRejected(@CheckForNull Class clazz, @CheckForNull String clazzName, String message) { + Throwable cause = null; + if (LOGGER.isLoggable(Level.FINE)) { + cause = new SecurityException("Class rejected by the class filter: " + + (clazz != null ? clazz.getName() : clazzName)); + } + LOGGER.log(Level.WARNING, message, cause); + + // TODO: add a Telemetry implementation (JEP-304) + } +} diff --git a/core/src/main/java/jenkins/security/ConfidentialStore.java b/core/src/main/java/jenkins/security/ConfidentialStore.java index 6e79d3d70a6dde0ede6f1e3e8564af750aeea0fe..1a8a152f82efe6c2ad61020ea12aa08d6299d2c7 100644 --- a/core/src/main/java/jenkins/security/ConfidentialStore.java +++ b/core/src/main/java/jenkins/security/ConfidentialStore.java @@ -4,7 +4,6 @@ import hudson.Extension; import hudson.Lookup; import hudson.init.InitMilestone; import hudson.util.Secret; -import hudson.util.Service; import jenkins.model.Jenkins; import org.kohsuke.MetaInfServices; @@ -12,7 +11,9 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import java.io.IOException; import java.security.SecureRandom; -import java.util.List; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; @@ -68,10 +69,11 @@ public abstract class ConfidentialStore { ConfidentialStore cs = lookup.get(ConfidentialStore.class); if (cs==null) { try { - List r = (List) Service.loadInstances(ConfidentialStore.class.getClassLoader(), ConfidentialStore.class); - if (!r.isEmpty()) - cs = r.get(0); - } catch (IOException e) { + Iterator it = ServiceLoader.load(ConfidentialStore.class, ConfidentialStore.class.getClassLoader()).iterator(); + if (it.hasNext()) { + cs = it.next(); + } + } catch (ServiceConfigurationError e) { LOGGER.log(Level.WARNING, "Failed to list up ConfidentialStore implementations",e); // fall through } diff --git a/core/src/main/java/jenkins/security/CustomClassFilter.java b/core/src/main/java/jenkins/security/CustomClassFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..f73068f097a60c2ac674cf3cedcd7e768e5378fc --- /dev/null +++ b/core/src/main/java/jenkins/security/CustomClassFilter.java @@ -0,0 +1,176 @@ +/* + * The MIT License + * + * Copyright 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.init.InitMilestone; +import hudson.init.Initializer; +import hudson.remoting.ClassFilter; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import org.apache.commons.io.IOUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Allows extensions to adjust the behavior of {@link ClassFilter#DEFAULT}. + * Custom filters can be called frequently, and return values are uncached, so implementations should be fast. + * @see ClassFilterImpl + * @since 2.102 + */ +public interface CustomClassFilter extends ExtensionPoint { + + /** + * Determine whether a class should be permitted by {@link ClassFilter#isBlacklisted(Class)} of {@link ClassFilter#DEFAULT}. + * @param c the class + * @return true to permit it when it would normally be rejected (for example due to having a custom serialization method and being from a third-party library); + * false to reject it when it would normally be permitted; + * null to express no opinion (the default) + */ + default @CheckForNull Boolean permits(Class c) { + return null; + } + + /** + * Determine whether a class should be permitted by {@link ClassFilter#isBlacklisted(String)} of {@link ClassFilter#DEFAULT}. + * @param name a class name + * @return true to permit it when it would normally be rejected (currently useless); + * false to reject it when it would normally be permitted (currently due to {@link ClassFilter#STANDARD}; + * null to express no opinion (the default) + */ + default @CheckForNull Boolean permits(String name) { + return null; + } + + /** + * Standard filter which pays attention to a system property. + * To use, specify a system property {@code hudson.remoting.ClassFilter} containing a comma-separated list of {@link Class#getName} to whitelist. + * Entries may also be preceded by {@code !} to blacklist. + * Example: {@code -Dhudson.remoting.ClassFilter=com.google.common.collect.LinkedListMultimap,!com.acme.illadvised.YoloReflectionFactory$Handle} + */ + @Restricted(NoExternalUse.class) + @Extension + public class Static implements CustomClassFilter { + + /** + * Map from {@link Class#getName} to true to permit, false to reject. + * Unmentioned classes are not treated specially. + * Intentionally {@code public} for possible mutation without restart by Groovy scripting. + */ + public final Map overrides = new HashMap<>(); + + public Static() { + String entries = SystemProperties.getString("hudson.remoting.ClassFilter"); + if (entries != null) { + for (String entry : entries.split(",")) { + if (entry.startsWith("!")) { + overrides.put(entry.substring(1), false); + } else { + overrides.put(entry, true); + } + } + Logger.getLogger(Static.class.getName()).log(Level.FINE, "user-defined entries: {0}", overrides); + } + } + + @Override + public Boolean permits(Class c) { + return permits(c.getName()); + } + + @Override + public Boolean permits(String name) { + return overrides.get(name); + } + + } + + /** + * Standard filter which can load whitelists and blacklists from plugins. + * To use, add a resource {@code META-INF/hudson.remoting.ClassFilter} to your plugin. + * Each line should be the {@link Class#getName} of a class to whitelist. + * Or you may blacklist a class by preceding its name with {@code !}. + * Example: + *

+     * com.google.common.collect.LinkedListMultimap
+     * !com.acme.illadvised.YoloReflectionFactory$Handle
+     * 
+ */ + @Restricted(NoExternalUse.class) + @Extension + public class Contributed implements CustomClassFilter { + + /** + * Map from {@link Class#getName} to true to permit, false to reject. + * Unmentioned classes are not treated specially. + */ + private final Map overrides = new HashMap<>(); + + @Override + public Boolean permits(Class c) { + return permits(c.getName()); + } + + @Override + public Boolean permits(String name) { + return overrides.get(name); + } + + @Initializer(after = InitMilestone.PLUGINS_PREPARED, before = InitMilestone.PLUGINS_STARTED, fatal = false) + public static void load() throws IOException { + Map overrides = ExtensionList.lookup(CustomClassFilter.class).get(Contributed.class).overrides; + overrides.clear(); + Enumeration resources = Jenkins.getInstance().getPluginManager().uberClassLoader.getResources("META-INF/hudson.remoting.ClassFilter"); + while (resources.hasMoreElements()) { + try (InputStream is = resources.nextElement().openStream()) { + for (String entry : IOUtils.readLines(is, StandardCharsets.UTF_8)) { + if (entry.matches("#.*|\\s*")) { + // skip + } else if (entry.startsWith("!")) { + overrides.put(entry.substring(1), false); + } else { + overrides.put(entry, true); + } + } + } + } + Logger.getLogger(Contributed.class.getName()).log(Level.FINE, "plugin-defined entries: {0}", overrides); + } + + } + +} diff --git a/core/src/main/java/jenkins/security/ExceptionTranslationFilter.java b/core/src/main/java/jenkins/security/ExceptionTranslationFilter.java index aaa71541764d1dc4b683c8094b675fbaa48236e5..24755588b027aab58b682429a39d57018e1d2a53 100644 --- a/core/src/main/java/jenkins/security/ExceptionTranslationFilter.java +++ b/core/src/main/java/jenkins/security/ExceptionTranslationFilter.java @@ -53,13 +53,13 @@ import java.util.logging.Logger; *

* If an {@link AuthenticationException} is detected, the filter will launch the authenticationEntryPoint. * This allows common handling of authentication failures originating from any subclass of - * AbstractSecurityInterceptor. + * {@code AbstractSecurityInterceptor}. *

*

* If an {@link AccessDeniedException} is detected, the filter will determine whether or not the user is an anonymous * user. If they are an anonymous user, the authenticationEntryPoint will be launched. If they are not - * an anonymous user, the filter will delegate to the AccessDeniedHandler. - * By default the filter will use AccessDeniedHandlerImpl. + * an anonymous user, the filter will delegate to the {@code AccessDeniedHandler}. + * By default the filter will use {@code AccessDeniedHandlerImpl}. *

*

* To use this filter, it is necessary to specify the following properties: @@ -74,7 +74,7 @@ import java.util.logging.Logger; * *

* Do not use this class directly. Instead configure - * web.xml to use the FilterToBeanProxy. + * web.xml to use the {@code FilterToBeanProxy}. *

* * @author Ben Alex diff --git a/core/src/main/java/jenkins/security/HMACConfidentialKey.java b/core/src/main/java/jenkins/security/HMACConfidentialKey.java index 71a0c2eb24d1fd2262c1b4cd34e4312df610ab94..3a83d5e213bd3af5177ab5ccd5da79ea03c58756 100644 --- a/core/src/main/java/jenkins/security/HMACConfidentialKey.java +++ b/core/src/main/java/jenkins/security/HMACConfidentialKey.java @@ -14,7 +14,7 @@ import java.util.Arrays; /** * {@link ConfidentialKey} that's used for creating a token by hashing some information with secret - * (such as hash(msg|secret)). + * (such as {@code hash(msg|secret)}). * *

* This provides more secure version of it by using HMAC. @@ -27,6 +27,7 @@ import java.util.Arrays; */ public class HMACConfidentialKey extends ConfidentialKey { private volatile SecretKey key; + private Mac mac; private final int length; /** @@ -61,12 +62,14 @@ public class HMACConfidentialKey extends ConfidentialKey { this(owner,shortName,Integer.MAX_VALUE); } - /** * Computes the message authentication code for the specified byte sequence. */ - public byte[] mac(byte[] message) { - return chop(createMac().doFinal(message)); + public synchronized byte[] mac(byte[] message) { + if (mac == null) { + mac = createMac(); + } + return chop(mac.doFinal(message)); } /** diff --git a/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java b/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java index 0a2ec20625cf108b983d76c75b46f4d8714dcbf4..5172188b1710f7c3dc64fe4a579d3c17c564e88e 100644 --- a/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java +++ b/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java @@ -48,14 +48,22 @@ public class LastGrantedAuthoritiesProperty extends UserProperty { public GrantedAuthority[] getAuthorities() { String[] roles = this.roles; // capture to a variable for immutability - GrantedAuthority[] r = new GrantedAuthority[roles==null ? 1 : roles.length+1]; - r[0] = SecurityRealm.AUTHENTICATED_AUTHORITY; - if (roles != null) { - for (int i = 1; i < r.length; i++) { - r[i] = new GrantedAuthorityImpl(roles[i - 1]); + if(roles == null){ + return new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}; + } + + String authenticatedRole = SecurityRealm.AUTHENTICATED_AUTHORITY.getAuthority(); + List grantedAuthorities = new ArrayList<>(roles.length + 1); + grantedAuthorities.add(new GrantedAuthorityImpl(authenticatedRole)); + + for (int i = 0; i < roles.length; i++){ + // to avoid having twice that role + if(!authenticatedRole.equals(roles[i])){ + grantedAuthorities.add(new GrantedAuthorityImpl(roles[i])); } } - return r; + + return grantedAuthorities.toArray(new GrantedAuthority[grantedAuthorities.size()]); } /** @@ -90,14 +98,6 @@ public class LastGrantedAuthoritiesProperty extends UserProperty { */ @Extension public static class SecurityListenerImpl extends SecurityListener { - @Override - protected void authenticated(@Nonnull UserDetails details) { - } - - @Override - protected void failedToAuthenticate(@Nonnull String username) { - } - @Override protected void loggedIn(@Nonnull String username) { try { @@ -143,10 +143,6 @@ public class LastGrantedAuthoritiesProperty extends UserProperty { // LOGGER.log(Level.WARNING, "Failed to record granted authorities",e); // } } - - @Override - protected void loggedOut(@Nonnull String username) { - } } @Extension @Symbol("lastGrantedAuthorities") diff --git a/core/src/main/java/jenkins/security/MasterToSlaveCallable.java b/core/src/main/java/jenkins/security/MasterToSlaveCallable.java index e348ec1e4903d3fb75b03de9baecf802203be937..d8ef8b09eed3b4f35dbb53ab9e95bf00d083f2ae 100644 --- a/core/src/main/java/jenkins/security/MasterToSlaveCallable.java +++ b/core/src/main/java/jenkins/security/MasterToSlaveCallable.java @@ -1,20 +1,50 @@ package jenkins.security; import hudson.remoting.Callable; +import hudson.remoting.Channel; +import hudson.remoting.ChannelClosedException; +import jenkins.slaves.RemotingVersionInfo; import org.jenkinsci.remoting.RoleChecker; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Convenient {@link Callable} meant to be run on agent. * + * Note that the logic within {@link #call()} should use API of a minimum supported Remoting version. + * See {@link RemotingVersionInfo#getMinimumSupportedVersion()}. + * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 + * @param the return type; note that this must either be defined in your plugin or included in the stock JEP-200 whitelist */ public abstract class MasterToSlaveCallable implements Callable { + + private static final long serialVersionUID = 1L; + @Override public void checkRoles(RoleChecker checker) throws SecurityException { checker.check(this,Roles.SLAVE); } - private static final long serialVersionUID = 1L; + //TODO: remove once Minimum supported Remoting version is 3.15 or above + @Override + public Channel getChannelOrFail() throws ChannelClosedException { + final Channel ch = Channel.current(); + if (ch == null) { + throw new ChannelClosedException(new IllegalStateException("No channel associated with the thread")); + } + return ch; + } + + //TODO: remove once Callable#getOpenChannelOrFail() once Minimaumsupported Remoting version is 3.15 or above + @Override + public Channel getOpenChannelOrFail() throws ChannelClosedException { + final Channel ch = getChannelOrFail(); + if (ch.isClosingOrClosed()) { // TODO: Since Remoting 2.33, we still need to explicitly declare minimum Remoting version + throw new ChannelClosedException(new IllegalStateException("The associated channel " + ch + " is closing down or has closed down", ch.getCloseRequestCause())); + } + return ch; + } } diff --git a/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java b/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java index 953c5ba69dac4a7fc188510394f84234bb34476b..b2dee77bd24e8a4a77ecb612954bf5cbca52f69e 100644 --- a/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java +++ b/core/src/main/java/jenkins/security/NotReallyRoleSensitiveCallable.java @@ -8,7 +8,7 @@ import org.jenkinsci.remoting.RoleChecker; * just as a convenient function that has parameterized return value and exception type. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public abstract class NotReallyRoleSensitiveCallable implements Callable { @Override diff --git a/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java b/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java index 7a2ef81427ec953c91c25bd0f0452f91eec733c5..c6283401b258cc17df0108b60f9ca57d817bf7e2 100644 --- a/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java +++ b/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java @@ -1,6 +1,7 @@ package jenkins.security; import hudson.Extension; +import hudson.model.PersistentDescriptor; import hudson.model.queue.Tasks; import hudson.util.DescribableList; import jenkins.model.GlobalConfiguration; @@ -21,21 +22,17 @@ import java.util.List; * @since 1.520 */ @Extension @Symbol("queueItemAuthenticator") -public class QueueItemAuthenticatorConfiguration extends GlobalConfiguration { +public class QueueItemAuthenticatorConfiguration extends GlobalConfiguration implements PersistentDescriptor { private final DescribableList authenticators = new DescribableList(this); - public QueueItemAuthenticatorConfiguration() { - load(); - } - private Object readResolve() { authenticators.setOwner(this); return this; } @Override - public GlobalConfigurationCategory getCategory() { + public @Nonnull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); } @@ -61,8 +58,8 @@ public class QueueItemAuthenticatorConfiguration extends GlobalConfiguration { } } - public static QueueItemAuthenticatorConfiguration get() { - return Jenkins.getInstance().getInjector().getInstance(QueueItemAuthenticatorConfiguration.class); + public static @Nonnull QueueItemAuthenticatorConfiguration get() { + return GlobalConfiguration.all().getInstance(QueueItemAuthenticatorConfiguration.class); } @Extension(ordinal = 100) diff --git a/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java b/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java new file mode 100644 index 0000000000000000000000000000000000000000..e7b0e7f7dfa8836755db7746885380c52ce2c3ef --- /dev/null +++ b/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java @@ -0,0 +1,118 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security; + +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.JsonInErrorMessageSanitizer; + +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Restricted(NoExternalUse.class) +public class RedactSecretJsonInErrorMessageSanitizer implements JsonInErrorMessageSanitizer { + private static final Logger LOGGER = Logger.getLogger(RedactSecretJsonInErrorMessageSanitizer.class.getName()); + + // must be kept in sync with hudson-behavior.js in function buildFormTree, password case + public static final String REDACT_KEY = "$redact"; + public static final String REDACT_VALUE = "[value redacted]"; + + public static final RedactSecretJsonInErrorMessageSanitizer INSTANCE = new RedactSecretJsonInErrorMessageSanitizer(); + + private RedactSecretJsonInErrorMessageSanitizer() {} + + @Override + public JSONObject sanitize(JSONObject jsonObject) { + return copyAndSanitizeObject(jsonObject); + } + + /** + * Accept anything as value for the {@link #REDACT_KEY} but only process the first level of an array and the string value. + */ + private Set retrieveRedactedKeys(JSONObject jsonObject) { + Set redactedKeySet = new HashSet<>(); + if (jsonObject.has(REDACT_KEY)) { + Object value = jsonObject.get(REDACT_KEY); + if (value instanceof JSONArray) { + for (Object o : jsonObject.getJSONArray(REDACT_KEY)) { + if (o instanceof String) { + redactedKeySet.add((String) o); + } else { + // array, object, null, number, boolean + LOGGER.log(Level.WARNING, "Unsupported type " + o.getClass().getName() + " for " + REDACT_KEY + ", please use either a single String value or an Array"); + } + } + } else if (value instanceof String) { + redactedKeySet.add((String) value); + } else { + // object, null, number, boolean + LOGGER.log(Level.WARNING, "Unsupported type " + value.getClass().getName() + " for " + REDACT_KEY + ", please use either a single String value or an Array"); + } + } + return redactedKeySet; + } + + private Object copyAndSanitize(Object value) { + if (value instanceof JSONObject) { + return copyAndSanitizeObject((JSONObject) value); + } else if (value instanceof JSONArray) { + return copyAndSanitizeArray((JSONArray) value); + } else { + // string, null, number, boolean + return value; + } + } + + @SuppressWarnings("unchecked") + private JSONObject copyAndSanitizeObject(JSONObject jsonObject) { + Set redactedKeySet = retrieveRedactedKeys(jsonObject); + JSONObject result = new JSONObject(); + + jsonObject.keySet().forEach(keyObject -> { + String key = keyObject.toString(); + if (redactedKeySet.contains(key)) { + result.accumulate(key, REDACT_VALUE); + } else { + Object value = jsonObject.get(keyObject); + result.accumulate(key, copyAndSanitize(value)); + } + }); + + return result; + } + + private JSONArray copyAndSanitizeArray(JSONArray jsonArray) { + JSONArray result = new JSONArray(); + + jsonArray.forEach(value -> + result.add(copyAndSanitize(value)) + ); + + return result; + } +} diff --git a/core/src/main/java/jenkins/security/Roles.java b/core/src/main/java/jenkins/security/Roles.java index ada2ca96c0899e331d573b32d4fbe944acab532f..e37b323accc2d7ef1f322c2d70fadd98a92da47d 100644 --- a/core/src/main/java/jenkins/security/Roles.java +++ b/core/src/main/java/jenkins/security/Roles.java @@ -12,7 +12,7 @@ import org.jenkinsci.remoting.Role; * not have any role. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public class Roles { /** diff --git a/core/src/main/java/jenkins/security/SecurityListener.java b/core/src/main/java/jenkins/security/SecurityListener.java index 7cec8c0eea4afa72f446c03d781d80592c0a5443..c473a85fb436c270a69c4fdb68e3c5072e444684 100644 --- a/core/src/main/java/jenkins/security/SecurityListener.java +++ b/core/src/main/java/jenkins/security/SecurityListener.java @@ -45,39 +45,51 @@ public abstract class SecurityListener implements ExtensionPoint { private static final Logger LOGGER = Logger.getLogger(SecurityListener.class.getName()); /** - * Fired when a user was successfully authenticated by password. - * This might be via the web UI, or via REST (not with an API token) or CLI (not with an SSH key). - * Only {@link AbstractPasswordBasedSecurityRealm}s are considered. - * @param details details of the newly authenticated user, such as name and groups + * Fired when a user was successfully authenticated using credentials. It could be password or any other credentials. + * This might be via the web UI, or via REST (using API token or Basic), or CLI (remoting, auth, ssh) + * or any other way plugins can propose. + * @param details details of the newly authenticated user, such as name and groups. */ - protected abstract void authenticated(@Nonnull UserDetails details); + protected void authenticated(@Nonnull UserDetails details){} /** - * Fired when a user tried to authenticate by password but failed. + * Fired when a user tried to authenticate but failed. + * In case the authentication method uses multiple layers to validate the credentials, + * we do fire this event only when even the last layer failed to authenticate. * @param username the user * @see #authenticated */ - protected abstract void failedToAuthenticate(@Nonnull String username); + protected void failedToAuthenticate(@Nonnull String username){} /** - * Fired when a user has logged in via the web UI. + * Fired when a user has logged in. Compared to authenticated, there is a notion of storage / cache. * Would be called after {@link #authenticated}. + * It should be called after the {@link org.acegisecurity.context.SecurityContextHolder#getContext()}'s authentication is set. * @param username the user */ - protected abstract void loggedIn(@Nonnull String username); + protected void loggedIn(@Nonnull String username){} /** - * Fired when a user has failed to log in via the web UI. + * @since TODO + * + * Fired after a new user account has been created and saved to disk. + * + * @param username the user + */ + protected void userCreated(@Nonnull String username) {} + + /** + * Fired when a user has failed to log in. * Would be called after {@link #failedToAuthenticate}. * @param username the user */ - protected abstract void failedToLogIn(@Nonnull String username); + protected void failedToLogIn(@Nonnull String username){} /** * Fired when a user logs out. * @param username the user */ - protected abstract void loggedOut(@Nonnull String username); + protected void loggedOut(@Nonnull String username){} /** @since 1.569 */ public static void fireAuthenticated(@Nonnull UserDetails details) { @@ -95,6 +107,14 @@ public abstract class SecurityListener implements ExtensionPoint { } } + /** @since TODO */ + public static void fireUserCreated(@Nonnull String username) { + LOGGER.log(Level.FINE, "new user created: {0}", username); + for (SecurityListener l : all()) { + l.userCreated(username); + } + } + /** @since 1.569 */ public static void fireFailedToAuthenticate(@Nonnull String username) { LOGGER.log(Level.FINE, "failed to authenticate: {0}", username); diff --git a/core/src/main/java/jenkins/security/SlaveToMasterCallable.java b/core/src/main/java/jenkins/security/SlaveToMasterCallable.java index 34b227c5bf847fe384b22c9688f096e78789cb1b..5da117224629b18aa3a0b2be932e34421dc2228d 100644 --- a/core/src/main/java/jenkins/security/SlaveToMasterCallable.java +++ b/core/src/main/java/jenkins/security/SlaveToMasterCallable.java @@ -6,9 +6,9 @@ import org.jenkinsci.remoting.RoleChecker; /** * Convenient {@link Callable} that are meant to run on the master (sent by agent/CLI/etc). - * + * Note that any serializable fields must either be defined in your plugin or included in the stock JEP-200 whitelist. * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public abstract class SlaveToMasterCallable implements Callable { @Override diff --git a/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java b/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java index a8295d8b75ef187189e086b5f0eda5cb06ac0f51..a6237022582d9bf1d5b704baf64858238614a3ed 100644 --- a/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java +++ b/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java @@ -26,6 +26,7 @@ package jenkins.security; import hudson.Extension; import hudson.PluginWrapper; +import hudson.model.PersistentDescriptor; import hudson.model.UpdateSite; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; @@ -50,19 +51,15 @@ import java.util.Set; */ @Extension @Restricted(NoExternalUse.class) -public class UpdateSiteWarningsConfiguration extends GlobalConfiguration { +public class UpdateSiteWarningsConfiguration extends GlobalConfiguration implements PersistentDescriptor { private HashSet ignoredWarnings = new HashSet<>(); @Override - public GlobalConfigurationCategory getCategory() { + public @Nonnull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); } - public UpdateSiteWarningsConfiguration() { - load(); - } - @Nonnull public Set getIgnoredWarnings() { return Collections.unmodifiableSet(ignoredWarnings); @@ -77,14 +74,14 @@ public class UpdateSiteWarningsConfiguration extends GlobalConfiguration { if (warning.type != UpdateSite.Warning.Type.PLUGIN) { return null; } - return Jenkins.getInstance().getPluginManager().getPlugin(warning.component); + return Jenkins.get().getPluginManager().getPlugin(warning.component); } @Nonnull public Set getAllWarnings() { HashSet allWarnings = new HashSet<>(); - for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) { + for (UpdateSite site : Jenkins.get().getUpdateCenter().getSites()) { UpdateSite.Data data = site.getData(); if (data != null) { allWarnings.addAll(data.getWarnings()); diff --git a/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java b/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java index 83c2828f1b370099e14af67adde134cf177538e3..ef376b0e5bf7b1d683bd144a0cab608baf931f33 100644 --- a/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java +++ b/core/src/main/java/jenkins/security/UpdateSiteWarningsMonitor.java @@ -121,12 +121,7 @@ public class UpdateSiteWarningsMonitor extends AdministrativeMonitor { } private Set getActiveWarnings() { - ExtensionList configurations = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class); - if (configurations.isEmpty()) { - return Collections.emptySet(); - } - UpdateSiteWarningsConfiguration configuration = configurations.get(0); - + UpdateSiteWarningsConfiguration configuration = ExtensionList.lookupSingleton(UpdateSiteWarningsConfiguration.class); HashSet activeWarnings = new HashSet<>(); for (UpdateSite.Warning warning : configuration.getApplicableWarnings()) { @@ -160,13 +155,7 @@ public class UpdateSiteWarningsMonitor extends AdministrativeMonitor { * @return true iff there are applicable but ignored (i.e. hidden) warnings. */ public boolean hasApplicableHiddenWarnings() { - ExtensionList configurations = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class); - if (configurations.isEmpty()) { - return false; - } - - UpdateSiteWarningsConfiguration configuration = configurations.get(0); - + UpdateSiteWarningsConfiguration configuration = ExtensionList.lookupSingleton(UpdateSiteWarningsConfiguration.class); return getActiveWarnings().size() < configuration.getApplicableWarnings().size(); } diff --git a/core/src/main/java/jenkins/security/UserDetailsCache.java b/core/src/main/java/jenkins/security/UserDetailsCache.java index 1d5f221e43bfba1fd42c63853b433e509564fff1..3e5394c2051b2ec20dcb60e10af33a142fbaba80 100644 --- a/core/src/main/java/jenkins/security/UserDetailsCache.java +++ b/core/src/main/java/jenkins/security/UserDetailsCache.java @@ -83,7 +83,7 @@ public final class UserDetailsCache { * @return the cache */ public static UserDetailsCache get() { - return ExtensionList.lookup(UserDetailsCache.class).get(UserDetailsCache.class); + return ExtensionList.lookupSingleton(UserDetailsCache.class); } /** diff --git a/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..e032f2818d905564ccb572488853718fd6d90575 --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java @@ -0,0 +1,97 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.apitoken; + +import hudson.Extension; +import hudson.model.PersistentDescriptor; +import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import org.jenkinsci.Symbol; + +/** + * Configuration for the new token generation when a user is created + * + * @since 2.129 + */ +@Extension +@Symbol("apiToken") +public class ApiTokenPropertyConfiguration extends GlobalConfiguration implements PersistentDescriptor { + /** + * When a user is created, this property determines whether or not we create a legacy token for the user. + * For security reasons, we do not recommend you enable this but we left that open to ease upgrades. + */ + private boolean tokenGenerationOnCreationEnabled = false; + + /** + * When a user has a legacy token, this property determines whether or not the user can request a new legacy token. + * For security reasons, we do not recommend you enable this but we left that open to ease upgrades. + */ + private boolean creationOfLegacyTokenEnabled = false; + + /** + * Each time an API Token is used, its usage counter is incremented and the last used date is updated. + * You can disable this feature using this property. + */ + private boolean usageStatisticsEnabled = true; + + public static ApiTokenPropertyConfiguration get() { + return GlobalConfiguration.all().get(ApiTokenPropertyConfiguration.class); + } + + public boolean hasExistingConfigFile(){ + return getConfigFile().exists(); + } + + public boolean isTokenGenerationOnCreationEnabled() { + return tokenGenerationOnCreationEnabled; + } + + public void setTokenGenerationOnCreationEnabled(boolean tokenGenerationOnCreationEnabled) { + this.tokenGenerationOnCreationEnabled = tokenGenerationOnCreationEnabled; + save(); + } + + public boolean isCreationOfLegacyTokenEnabled() { + return creationOfLegacyTokenEnabled; + } + + public void setCreationOfLegacyTokenEnabled(boolean creationOfLegacyTokenEnabled) { + this.creationOfLegacyTokenEnabled = creationOfLegacyTokenEnabled; + save(); + } + + public boolean isUsageStatisticsEnabled() { + return usageStatisticsEnabled; + } + + public void setUsageStatisticsEnabled(boolean usageStatisticsEnabled) { + this.usageStatisticsEnabled = usageStatisticsEnabled; + save(); + } + + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } +} diff --git a/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyDisabledDefaultAdministrativeMonitor.java b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyDisabledDefaultAdministrativeMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..b5afdca805628739403c4dab810cc9082dd627df --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyDisabledDefaultAdministrativeMonitor.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.apitoken; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import hudson.util.HttpResponses; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import java.io.IOException; + +/** + * Monitor that the API Token are not generated by default without the user interaction. + */ +@Extension +@Symbol("apiTokenLegacyAutoGeneration") +@Restricted(NoExternalUse.class) +public class ApiTokenPropertyDisabledDefaultAdministrativeMonitor extends AdministrativeMonitor { + @Override + public String getDisplayName() { + return Messages.ApiTokenPropertyDisabledDefaultAdministrativeMonitor_displayName(); + } + + @Override + public boolean isActivated() { + return ApiTokenPropertyConfiguration.get().isTokenGenerationOnCreationEnabled(); + } + + @RequirePOST + public HttpResponse doAct(@QueryParameter String no) throws IOException { + if (no == null) { + ApiTokenPropertyConfiguration.get().setTokenGenerationOnCreationEnabled(false); + } else { + disable(true); + } + return HttpResponses.redirectViaContextPath("manage"); + } +} diff --git a/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor.java b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..2cae664d0878161def7bf9312a6564509af2da73 --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.apitoken; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import hudson.util.HttpResponses; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import java.io.IOException; + +/** + * Monitor that the API Token cannot be created for a user without existing legacy token + */ +@Extension +@Symbol("apiTokenNewLegacyWithoutExisting") +@Restricted(NoExternalUse.class) +public class ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor extends AdministrativeMonitor { + @Override + public String getDisplayName() { + return Messages.ApiTokenPropertyEnabledNewLegacyAdministrativeMonitor_displayName(); + } + + @Override + public boolean isActivated() { + return ApiTokenPropertyConfiguration.get().isCreationOfLegacyTokenEnabled(); + } + + @RequirePOST + public HttpResponse doAct(@QueryParameter String no) throws IOException { + if (no == null) { + ApiTokenPropertyConfiguration.get().setCreationOfLegacyTokenEnabled(false); + } else { + disable(true); + } + return HttpResponses.redirectViaContextPath("manage"); + } +} diff --git a/core/src/main/java/jenkins/security/apitoken/ApiTokenStats.java b/core/src/main/java/jenkins/security/apitoken/ApiTokenStats.java new file mode 100644 index 0000000000000000000000000000000000000000..1daea83f4c1f6b1e08cfb13382f90db564039262 --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenStats.java @@ -0,0 +1,337 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.apitoken; + +import com.google.common.annotations.VisibleForTesting; +import hudson.BulkChange; +import hudson.Util; +import hudson.XmlFile; +import hudson.model.Saveable; +import hudson.model.User; +import hudson.model.listeners.SaveableListener; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Restricted(NoExternalUse.class) +public class ApiTokenStats implements Saveable { + private static final Logger LOGGER = Logger.getLogger(ApiTokenStats.class.getName()); + + /** + * Normally a user will not have more 2-3 tokens at a time, + * so there is no need to store a map here + */ + private List tokenStats; + + private transient User user; + + @VisibleForTesting + transient File parent; + + @VisibleForTesting + ApiTokenStats() { + this.init(); + } + + private Object readResolve() { + this.init(); + return this; + } + + private void init() { + if (this.tokenStats == null) { + this.tokenStats = new ArrayList<>(); + } else { + keepLastUpdatedUnique(); + } + } + + /** + * In case of duplicate entries, we keep only the last updated element + */ + private void keepLastUpdatedUnique() { + Map temp = new HashMap<>(); + this.tokenStats.forEach(candidate -> { + SingleTokenStats current = temp.get(candidate.tokenUuid); + if (current == null) { + temp.put(candidate.tokenUuid, candidate); + } else { + int comparison = SingleTokenStats.COMP_BY_LAST_USE_THEN_COUNTER.compare(current, candidate); + if (comparison < 0) { + // candidate was updated more recently (or has a bigger counter in case of perfectly equivalent dates) + temp.put(candidate.tokenUuid, candidate); + } + } + }); + + this.tokenStats = new ArrayList<>(temp.values()); + } + + /** + * @deprecated use {@link #load(User)} instead of {@link #load(File)} + * The method will be removed in a later version as it's an internal one + */ + @Deprecated + // to force even if someone wants to remove the one from the class + @Restricted(NoExternalUse.class) + void setParent(@Nonnull File parent) { + this.parent = parent; + } + + private boolean areStatsDisabled(){ + return !ApiTokenPropertyConfiguration.get().isUsageStatisticsEnabled(); + } + + /** + * Will trigger the save if there is some modification + */ + public synchronized void removeId(@Nonnull String tokenUuid) { + if(areStatsDisabled()){ + return; + } + + boolean tokenRemoved = tokenStats.removeIf(s -> s.tokenUuid.equals(tokenUuid)); + if (tokenRemoved) { + save(); + } + } + + /** + * Will trigger the save + */ + public synchronized @Nonnull SingleTokenStats updateUsageForId(@Nonnull String tokenUuid) { + if(areStatsDisabled()){ + return new SingleTokenStats(tokenUuid); + } + + SingleTokenStats stats = findById(tokenUuid) + .orElseGet(() -> { + SingleTokenStats result = new SingleTokenStats(tokenUuid); + tokenStats.add(result); + return result; + }); + + stats.notifyUse(); + save(); + + return stats; + } + + public synchronized @Nonnull SingleTokenStats findTokenStatsById(@Nonnull String tokenUuid) { + if(areStatsDisabled()){ + return new SingleTokenStats(tokenUuid); + } + + // if we create a new empty stats object, no need to add it to the list + return findById(tokenUuid) + .orElse(new SingleTokenStats(tokenUuid)); + } + + private @Nonnull Optional findById(@Nonnull String tokenUuid) { + return tokenStats.stream() + .filter(s -> s.tokenUuid.equals(tokenUuid)) + .findFirst(); + } + + /** + * Saves the configuration info to the disk. + */ + @Override + public synchronized void save() { + if(areStatsDisabled()){ + return; + } + + if (BulkChange.contains(this)) + return; + + /* + * Note: the userFolder should never be null at this point. + * The userFolder could be null during User creation with the new storage approach + * but when this code is called, from token used / removed, the folder exists. + */ + File userFolder = getUserFolder(); + if (userFolder == null) { + return; + } + + XmlFile configFile = getConfigFile(userFolder); + try { + configFile.write(this); + SaveableListener.fireOnChange(this, configFile); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to save " + configFile, e); + } + } + + private @CheckForNull File getUserFolder(){ + File userFolder = parent; + if (userFolder == null && this.user != null) { + userFolder = user.getUserFolder(); + if (userFolder == null) { + LOGGER.log(Level.INFO, "No user folder yet for user {0}", user.getId()); + return null; + } + this.parent = userFolder; + } + + return userFolder; + } + + /** + * Loads the data from the disk into the new object. + *

+ * If the file is not present, a fresh new instance is created. + * + * @deprecated use {@link #load(User)} instead + * The method will be removed in a later version as it's an internal one + */ + @Deprecated + // to force even if someone wants to remove the one from the class + @Restricted(NoExternalUse.class) + public static @Nonnull ApiTokenStats load(@CheckForNull File parent) { + // even if we are not using statistics, we load the existing one in case the configuration + // is enabled afterwards to avoid erasing data + + if (parent == null) { + return new ApiTokenStats(); + } + + ApiTokenStats apiTokenStats = internalLoad(parent); + if (apiTokenStats == null) { + apiTokenStats = new ApiTokenStats(); + } + + apiTokenStats.setParent(parent); + return apiTokenStats; + } + + /** + * Loads the data from the user folder into the new object. + *

+ * If the folder does not exist yet, a fresh new instance is created. + */ + public static @Nonnull ApiTokenStats load(@Nonnull User user) { + // even if we are not using statistics, we load the existing one in case the configuration + // is enabled afterwards to avoid erasing data + + ApiTokenStats apiTokenStats = null; + + File userFolder = user.getUserFolder(); + if (userFolder != null) { + apiTokenStats = internalLoad(userFolder); + } + + if (apiTokenStats == null) { + apiTokenStats = new ApiTokenStats(); + } + + apiTokenStats.user = user; + + return apiTokenStats; + } + + @VisibleForTesting + static @CheckForNull ApiTokenStats internalLoad(@Nonnull File userFolder) { + ApiTokenStats apiTokenStats = null; + XmlFile statsFile = getConfigFile(userFolder); + if (statsFile.exists()) { + try { + apiTokenStats = (ApiTokenStats) statsFile.unmarshal(ApiTokenStats.class); + apiTokenStats.parent = userFolder; + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to load " + statsFile, e); + } + } + + return apiTokenStats; + } + + protected static @Nonnull XmlFile getConfigFile(@Nonnull File parent) { + return new XmlFile(new File(parent, "apiTokenStats.xml")); + } + + public static class SingleTokenStats { + private static Comparator COMP_BY_LAST_USE_THEN_COUNTER = + Comparator.comparing(SingleTokenStats::getLastUseDate, Comparator.nullsFirst(Comparator.naturalOrder())) + .thenComparing(SingleTokenStats::getUseCounter); + + private final String tokenUuid; + private Date lastUseDate; + private Integer useCounter; + + private SingleTokenStats(String tokenUuid) { + this.tokenUuid = tokenUuid; + } + + private Object readResolve() { + if (this.useCounter != null) { + // to avoid negative numbers to be injected + this.useCounter = Math.max(0, this.useCounter); + } + return this; + } + + private void notifyUse() { + this.useCounter = useCounter == null ? 1 : useCounter + 1; + this.lastUseDate = new Date(); + } + + public String getTokenUuid() { + return tokenUuid; + } + + // used by Jelly view + public int getUseCounter() { + return useCounter == null ? 0 : useCounter; + } + + // used by Jelly view + public Date getLastUseDate() { + return lastUseDate; + } + + // used by Jelly view + /** + * Return the number of days since the last usage + * Relevant only if the lastUseDate is not null + */ + public long getNumDaysUse() { + return lastUseDate == null ? 0 : Util.daysElapsedSince(lastUseDate); + } + } +} diff --git a/core/src/main/java/jenkins/security/apitoken/ApiTokenStore.java b/core/src/main/java/jenkins/security/apitoken/ApiTokenStore.java new file mode 100644 index 0000000000000000000000000000000000000000..4e4f4ca0ca229f28684706cfaf4cf97a72b2165e --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenStore.java @@ -0,0 +1,444 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.apitoken; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Util; +import hudson.util.Secret; +import jenkins.security.Messages; +import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +@Restricted(NoExternalUse.class) +public class ApiTokenStore { + private static final Logger LOGGER = Logger.getLogger(ApiTokenStore.class.getName()); + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final Comparator SORT_BY_LOWERCASED_NAME = + Comparator.comparing(hashedToken -> hashedToken.getName().toLowerCase(Locale.ENGLISH)); + + private static final int TOKEN_LENGTH_V2 = 34; + /** two hex characters, avoid starting with 0 to avoid troubles */ + private static final String LEGACY_VERSION = "10"; + private static final String HASH_VERSION = "11"; + + private static final String HASH_ALGORITHM = "SHA-256"; + + private List tokenList; + + public ApiTokenStore() { + this.init(); + } + + private Object readResolve() { + this.init(); + return this; + } + + private void init() { + if (this.tokenList == null) { + this.tokenList = new ArrayList<>(); + } + } + + @SuppressFBWarnings("NP_NONNULL_RETURN_VIOLATION") + public synchronized @Nonnull Collection getTokenListSortedByName() { + return tokenList.stream() + .sorted(SORT_BY_LOWERCASED_NAME) + .collect(Collectors.toList()); + } + + private void addToken(HashedToken token) { + this.tokenList.add(token); + } + + /** + * Defensive approach to avoid involuntary change since the UUIDs are generated at startup only for UI + * and so between restart they change + */ + public synchronized void reconfigure(@Nonnull Map tokenStoreDataMap) { + tokenList.forEach(hashedToken -> { + JSONObject receivedTokenData = tokenStoreDataMap.get(hashedToken.uuid); + if (receivedTokenData == null) { + LOGGER.log(Level.INFO, "No token received for {0}", hashedToken.uuid); + return; + } + + String name = receivedTokenData.getString("tokenName"); + if (StringUtils.isBlank(name)) { + LOGGER.log(Level.INFO, "Empty name received for {0}, we do not care about it", hashedToken.uuid); + return; + } + + hashedToken.setName(name); + }); + } + + /** + * Remove the legacy token present and generate a new one using the given secret. + */ + public synchronized void regenerateTokenFromLegacy(@Nonnull Secret newLegacyApiToken) { + deleteAllLegacyAndGenerateNewOne(newLegacyApiToken, false); + } + + /** + * Same as {@link #regenerateTokenFromLegacy(Secret)} but only applied if there is an existing legacy token. + *

+ * Otherwise, no effect. + */ + public synchronized void regenerateTokenFromLegacyIfRequired(@Nonnull Secret newLegacyApiToken) { + if(tokenList.stream().noneMatch(HashedToken::isLegacy)){ + deleteAllLegacyAndGenerateNewOne(newLegacyApiToken, true); + } + } + + private void deleteAllLegacyAndGenerateNewOne(@Nonnull Secret newLegacyApiToken, boolean migrationFromExistingLegacy) { + deleteAllLegacyTokens(); + addLegacyToken(newLegacyApiToken, migrationFromExistingLegacy); + } + + private void deleteAllLegacyTokens() { + // normally there is only one, but just in case + tokenList.removeIf(HashedToken::isLegacy); + } + + private void addLegacyToken(@Nonnull Secret legacyToken, boolean migrationFromExistingLegacy) { + String tokenUserUseNormally = Util.getDigestOf(legacyToken.getPlainText()); + + String secretValueHashed = this.plainSecretToHashInHex(tokenUserUseNormally); + + HashValue hashValue = new HashValue(LEGACY_VERSION, secretValueHashed); + HashedToken token = HashedToken.buildNewFromLegacy(hashValue, migrationFromExistingLegacy); + + this.addToken(token); + } + + /** + * @return {@code null} iff there is no legacy token in the store, otherwise the legacy token is returned + */ + public synchronized @Nullable HashedToken getLegacyToken(){ + return tokenList.stream() + .filter(HashedToken::isLegacy) + .findFirst() + .orElse(null); + } + + /** + * Create a new token with the given name and return it id and secret value. + * Result meant to be sent / displayed and then discarded. + */ + public synchronized @Nonnull TokenUuidAndPlainValue generateNewToken(@Nonnull String name) { + // 16x8=128bit worth of randomness, using brute-force you need on average 2^127 tries (~10^37) + byte[] random = new byte[16]; + RANDOM.nextBytes(random); + String secretValue = Util.toHexString(random); + String tokenTheUserWillUse = HASH_VERSION + secretValue; + assert tokenTheUserWillUse.length() == 2 + 32; + + String secretValueHashed = this.plainSecretToHashInHex(secretValue); + + HashValue hashValue = new HashValue(HASH_VERSION, secretValueHashed); + HashedToken token = HashedToken.buildNew(name, hashValue); + + this.addToken(token); + + return new TokenUuidAndPlainValue(token.uuid, tokenTheUserWillUse); + } + + @SuppressFBWarnings("NP_NONNULL_RETURN_VIOLATION") + private @Nonnull String plainSecretToHashInHex(@Nonnull String secretValueInPlainText) { + byte[] hashBytes = plainSecretToHashBytes(secretValueInPlainText); + return Util.toHexString(hashBytes); + } + + private @Nonnull byte[] plainSecretToHashBytes(@Nonnull String secretValueInPlainText) { + // ascii is sufficient for hex-format + return hashedBytes(secretValueInPlainText.getBytes(StandardCharsets.US_ASCII)); + } + + private @Nonnull byte[] hashedBytes(byte[] tokenBytes) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance(HASH_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("There is no " + HASH_ALGORITHM + " available in this system"); + } + return digest.digest(tokenBytes); + } + + /** + * Search in the store if there is a token with the same secret as the one given + * @return {@code null} iff there is no matching token + */ + public synchronized @CheckForNull HashedToken findMatchingToken(@Nonnull String token) { + String plainToken; + if (isLegacyToken(token)) { + plainToken = token; + } else { + plainToken = getHashOfToken(token); + } + + return searchMatch(plainToken); + } + + /** + * Determine if the given token was generated by the legacy system or the new one + */ + private boolean isLegacyToken(@Nonnull String token) { + return token.length() != TOKEN_LENGTH_V2; + } + + /** + * Retrieve the hash part of the token + * @param token assumed the token is not a legacy one and represent the full token (version + hash) + * @return the hash part + */ + private @Nonnull String getHashOfToken(@Nonnull String token) { + /* + * Structure of the token: + * + * [2: version][32: real token] + * ------------^^^^^^^^^^^^^^^^ + */ + return token.substring(2); + } + + /** + * Search in the store if there is a matching token that has the same secret. + * @return {@code null} iff there is no matching token + */ + private @CheckForNull HashedToken searchMatch(@Nonnull String plainSecret) { + byte[] hashedBytes = plainSecretToHashBytes(plainSecret); + for (HashedToken token : tokenList) { + if (token.match(hashedBytes)) { + return token; + } + } + + return null; + } + + /** + * Remove a token given its identifier. Effectively make it unusable for future connection. + * + * @param tokenUuid The identifier of the token, could be retrieved directly from the {@link HashedToken#getUuid()} + * @return the revoked token corresponding to the given {@code tokenUuid} if one was found, otherwise {@code null} + */ + public synchronized @CheckForNull HashedToken revokeToken(@Nonnull String tokenUuid) { + for (Iterator iterator = tokenList.iterator(); iterator.hasNext(); ) { + HashedToken token = iterator.next(); + if (token.uuid.equals(tokenUuid)) { + iterator.remove(); + + return token; + } + } + + return null; + } + + /** + * Given a token identifier and a name, the system will try to find a corresponding token and rename it + * @return {@code true} iff the token was found and the rename was successful + */ + public synchronized boolean renameToken(@Nonnull String tokenUuid, @Nonnull String newName) { + for (HashedToken token : tokenList) { + if (token.uuid.equals(tokenUuid)) { + token.rename(newName); + return true; + } + } + + LOGGER.log(Level.FINER, "The target token for rename does not exist, for uuid = {0}, with desired name = {1}", new Object[]{tokenUuid, newName}); + return false; + } + + @Immutable + private static class HashValue implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Allow to distinguish tokens from different versions easily to adapt the logic + */ + private final String version; + /** + * Only confidential information in this class. It's a SHA-256 hash stored in hex format + */ + private final String hash; + + private HashValue(String version, String hash) { + this.version = version; + this.hash = hash; + } + } + + /** + * Contains information about the token and the secret value. + * It should not be stored as is, but just displayed once to the user and then forget about it. + */ + @Immutable + public static class TokenUuidAndPlainValue { + /** + * The token identifier to allow manipulation of the token + */ + public final String tokenUuid; + + /** + * Confidential information, must not be stored.

+ * It's meant to be send only one to the user and then only store the hash of this value. + */ + public final String plainValue; + + private TokenUuidAndPlainValue(String tokenUuid, String plainValue) { + this.tokenUuid = tokenUuid; + this.plainValue = plainValue; + } + } + + public static class HashedToken implements Serializable { + + private static final long serialVersionUID = 1L; + + // allow us to rename the token and link the statistics + private String uuid; + private String name; + private Date creationDate; + + private HashValue value; + + private HashedToken() { + this.init(); + } + + private Object readResolve() { + this.init(); + return this; + } + + private void init() { + if(this.uuid == null){ + this.uuid = UUID.randomUUID().toString(); + } + } + + public static @Nonnull HashedToken buildNew(@Nonnull String name, @Nonnull HashValue value) { + HashedToken result = new HashedToken(); + result.name = name; + result.creationDate = new Date(); + + result.value = value; + + return result; + } + + public static @Nonnull HashedToken buildNewFromLegacy(@Nonnull HashValue value, boolean migrationFromExistingLegacy) { + HashedToken result = new HashedToken(); + result.name = Messages.ApiTokenProperty_LegacyTokenName(); + if(migrationFromExistingLegacy){ + // we do not know when the legacy token was created + result.creationDate = null; + }else{ + // it comes from a manual action, so we set the creation date to now + result.creationDate = new Date(); + } + + result.value = value; + + return result; + } + + public void rename(String newName) { + this.name = newName; + } + + public boolean match(byte[] hashedBytes) { + byte[] hashFromHex; + try { + hashFromHex = Util.fromHexString(value.hash); + } catch (NumberFormatException e) { + LOGGER.log(Level.INFO, "The API token with name=[{0}] is not in hex-format and so cannot be used", name); + return false; + } + + // String.equals() is not constant-time but this method is. No link between correctness and time spent + return MessageDigest.isEqual(hashFromHex, hashedBytes); + } + + // used by Jelly view + public String getName() { + return name; + } + + // used by Jelly view + public Date getCreationDate() { + return creationDate; + } + + // used by Jelly view + /** + * Relevant only if the lastUseDate is not null + */ + public long getNumDaysCreation() { + return creationDate == null ? 0 : Util.daysElapsedSince(creationDate); + } + + // used by Jelly view + public String getUuid() { + return this.uuid; + } + + public boolean isLegacy() { + return this.value.version.equals(LEGACY_VERSION); + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/core/src/main/java/jenkins/security/apitoken/LegacyApiTokenAdministrativeMonitor.java b/core/src/main/java/jenkins/security/apitoken/LegacyApiTokenAdministrativeMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..8e56ceb40b3ef1a1e3952645b67c6108a6840026 --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/LegacyApiTokenAdministrativeMonitor.java @@ -0,0 +1,200 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.apitoken; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import hudson.model.User; +import hudson.node_monitors.AbstractAsyncNodeMonitorDescriptor; +import hudson.util.HttpResponses; +import jenkins.security.ApiTokenProperty; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpRedirect; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.json.JsonBody; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Monitor the list of users that still have legacy token + */ +@Extension +@Symbol("legacyApiTokenUsage") +@Restricted(NoExternalUse.class) +public class LegacyApiTokenAdministrativeMonitor extends AdministrativeMonitor { + private static final Logger LOGGER = Logger.getLogger(AbstractAsyncNodeMonitorDescriptor.class.getName()); + + public LegacyApiTokenAdministrativeMonitor() { + super("legacyApiToken"); + } + + @Override + public String getDisplayName() { + return Messages.LegacyApiTokenAdministrativeMonitor_displayName(); + } + + @Override + public boolean isActivated() { + return User.getAll().stream() + .anyMatch(user -> { + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + return (apiTokenProperty != null && apiTokenProperty.hasLegacyToken()); + }); + } + + public HttpResponse doIndex() throws IOException { + return new HttpRedirect("manage"); + } + + // used by Jelly view + @Restricted(NoExternalUse.class) + public List getImpactedUserList() { + return User.getAll().stream() + .filter(user -> { + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + return (apiTokenProperty != null && apiTokenProperty.hasLegacyToken()); + }) + .collect(Collectors.toList()); + } + + // used by Jelly view + @Restricted(NoExternalUse.class) + public @Nullable ApiTokenStore.HashedToken getLegacyTokenOf(@Nonnull User user) { + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + ApiTokenStore.HashedToken legacyToken = apiTokenProperty.getTokenStore().getLegacyToken(); + return legacyToken; + } + + // used by Jelly view + @Restricted(NoExternalUse.class) + public @Nullable ApiTokenProperty.TokenInfoAndStats getLegacyStatsOf(@Nonnull User user, @Nullable ApiTokenStore.HashedToken legacyToken) { + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + if (legacyToken != null) { + ApiTokenStats.SingleTokenStats legacyStats = apiTokenProperty.getTokenStats().findTokenStatsById(legacyToken.getUuid()); + ApiTokenProperty.TokenInfoAndStats tokenInfoAndStats = new ApiTokenProperty.TokenInfoAndStats(legacyToken, legacyStats); + return tokenInfoAndStats; + } + + // in case the legacy token was revoked during the request + return null; + } + + /** + * Determine if the user has at least one "new" token that was created after the last use of the legacy token + */ + // used by Jelly view + @Restricted(NoExternalUse.class) + public boolean hasFreshToken(@Nonnull User user, @Nullable ApiTokenProperty.TokenInfoAndStats legacyStats) { + if (legacyStats == null) { + return false; + } + + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + + return apiTokenProperty.getTokenList().stream() + .filter(token -> !token.isLegacy) + .anyMatch(token -> { + Date creationDate = token.creationDate; + Date lastUseDate = legacyStats.lastUseDate; + if (lastUseDate == null) { + lastUseDate = legacyStats.creationDate; + } + return creationDate != null && lastUseDate != null && creationDate.after(lastUseDate); + }); + } + + /** + * Determine if the user has at least one "new" token that was used after the last use of the legacy token + */ + // used by Jelly view + @Restricted(NoExternalUse.class) + public boolean hasMoreRecentlyUsedToken(@Nonnull User user, @Nullable ApiTokenProperty.TokenInfoAndStats legacyStats) { + if (legacyStats == null) { + return false; + } + + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + + return apiTokenProperty.getTokenList().stream() + .filter(token -> !token.isLegacy) + .anyMatch(token -> { + Date currentLastUseDate = token.lastUseDate; + Date legacyLastUseDate = legacyStats.lastUseDate; + if (legacyLastUseDate == null) { + legacyLastUseDate = legacyStats.creationDate; + } + return currentLastUseDate != null && legacyLastUseDate != null && currentLastUseDate.after(legacyLastUseDate); + }); + } + + @RequirePOST + public HttpResponse doRevokeAllSelected(@JsonBody RevokeAllSelectedModel content) throws IOException { + for (RevokeAllSelectedUserAndUuid value : content.values) { + if (value.userId == null) { + // special case not managed by JSONObject + value.userId = "null"; + } + User user = User.getById(value.userId, false); + if (user == null) { + LOGGER.log(Level.INFO, "User not found id={0}", value.userId); + } else { + ApiTokenProperty apiTokenProperty = user.getProperty(ApiTokenProperty.class); + if (apiTokenProperty == null) { + LOGGER.log(Level.INFO, "User without apiTokenProperty found id={0}", value.userId); + } else { + ApiTokenStore.HashedToken revokedToken = apiTokenProperty.getTokenStore().revokeToken(value.uuid); + if (revokedToken == null) { + LOGGER.log(Level.INFO, "User without selected token id={0}, tokenUuid={1}", new Object[]{value.userId, value.uuid}); + } else { + apiTokenProperty.deleteApiToken(); + user.save(); + LOGGER.log(Level.INFO, "Revocation success for user id={0}, tokenUuid={1}", new Object[]{value.userId, value.uuid}); + } + } + } + } + return HttpResponses.ok(); + } + + @Restricted(NoExternalUse.class) + public static final class RevokeAllSelectedModel { + public RevokeAllSelectedUserAndUuid[] values; + } + + @Restricted(NoExternalUse.class) + public static final class RevokeAllSelectedUserAndUuid { + public String userId; + public String uuid; + } +} diff --git a/core/src/main/java/jenkins/security/csrf/CSRFAdministrativeMonitor.java b/core/src/main/java/jenkins/security/csrf/CSRFAdministrativeMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..6ffc2422e42caca00832bddd486921344e86c2d1 --- /dev/null +++ b/core/src/main/java/jenkins/security/csrf/CSRFAdministrativeMonitor.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.csrf; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Monitor that the CSRF protection is enabled on the application. + * + * @since 2.85 + */ +@Extension +@Symbol("csrf") +@Restricted(NoExternalUse.class) +public class CSRFAdministrativeMonitor extends AdministrativeMonitor { + @Override + public String getDisplayName() { + return Messages.CSRFAdministrativeMonitor_displayName(); + } + + @Override + public boolean isActivated() { + return Jenkins.get().getCrumbIssuer() == null; + } +} diff --git a/core/src/main/java/jenkins/security/s2m/AdminCallableMonitor.java b/core/src/main/java/jenkins/security/s2m/AdminCallableMonitor.java index 3a9d095af1522ad81371ee4373852cd84949e11f..8f491d29293bbead9dc16e3c6cc70681e406f42d 100644 --- a/core/src/main/java/jenkins/security/s2m/AdminCallableMonitor.java +++ b/core/src/main/java/jenkins/security/s2m/AdminCallableMonitor.java @@ -18,7 +18,7 @@ import java.io.IOException; * Report any rejected {@link Callable}s and {@link FilePath} executions and allow * admins to whitelist them. * - * @since 1.THU + * @since 1.587 / 1.580.1 * @author Kohsuke Kawaguchi */ @Extension @Symbol("slaveToMasterAccessControl") diff --git a/core/src/main/java/jenkins/security/s2m/AdminFilePathFilter.java b/core/src/main/java/jenkins/security/s2m/AdminFilePathFilter.java index 7832f82bbb335789bbeabfd1dcd80a3363ae5fd9..d5d94b52d5d786e86c0af2e6f5c979c486f6722d 100644 --- a/core/src/main/java/jenkins/security/s2m/AdminFilePathFilter.java +++ b/core/src/main/java/jenkins/security/s2m/AdminFilePathFilter.java @@ -17,7 +17,7 @@ import java.io.File; * This class is just a glue, and the real logic happens inside {@link AdminWhitelistRule} * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public class AdminFilePathFilter extends ReflectiveFilePathFilter { diff --git a/core/src/main/java/jenkins/security/s2m/AdminWhitelistRule.java b/core/src/main/java/jenkins/security/s2m/AdminWhitelistRule.java index a05fd59e8ebc58c7ab1d112318f8a2f709144390..0a3eb62ffdd1f248c6981dfbdfa065635e766da2 100644 --- a/core/src/main/java/jenkins/security/s2m/AdminWhitelistRule.java +++ b/core/src/main/java/jenkins/security/s2m/AdminWhitelistRule.java @@ -16,6 +16,8 @@ import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.interceptor.RequirePOST; +import javax.annotation.CheckReturnValue; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -52,12 +54,10 @@ public class AdminWhitelistRule implements StaplerProxy { */ public final FilePathRuleConfig filePathRules; - private final Jenkins jenkins; - private boolean masterKillSwitch; public AdminWhitelistRule() throws IOException, InterruptedException { - this.jenkins = Jenkins.getInstance(); + final Jenkins jenkins = Jenkins.get(); // while this file is not a secret, write access to this file is dangerous, // so put this in the better-protected part of $JENKINS_HOME, which is in secrets/ @@ -79,17 +79,21 @@ public class AdminWhitelistRule implements StaplerProxy { whitelisted); this.filePathRules = new FilePathRuleConfig( new File(jenkins.getRootDir(),"secrets/filepath-filters.d/50-gui.conf")); - this.masterKillSwitch = loadMasterKillSwitchFile(); + + File f = getMasterKillSwitchFile(jenkins); + this.masterKillSwitch = loadMasterKillSwitchFile(f); } /** - * Reads the master kill switch. + * Reads the master kill switch from a file. * * Instead of {@link FileBoolean}, we use a text file so that the admin can prevent Jenkins from * writing this to file. + * @param f File to load + * @return {@code true} if the file was loaded, {@code false} otherwise */ - private boolean loadMasterKillSwitchFile() { - File f = getMasterKillSwitchFile(); + @CheckReturnValue + private boolean loadMasterKillSwitchFile(@Nonnull File f) { try { if (!f.exists()) return true; return Boolean.parseBoolean(FileUtils.readFileToString(f).trim()); @@ -99,7 +103,8 @@ public class AdminWhitelistRule implements StaplerProxy { } } - private File getMasterKillSwitchFile() { + @Nonnull + private File getMasterKillSwitchFile(@Nonnull Jenkins jenkins) { return new File(jenkins.getRootDir(),"secrets/slave-to-master-security-kill-switch"); } @@ -155,8 +160,6 @@ public class AdminWhitelistRule implements StaplerProxy { @RequirePOST public HttpResponse doSubmit(StaplerRequest req) throws IOException { - jenkins.checkPermission(Jenkins.RUN_SCRIPTS); - String whitelist = Util.fixNull(req.getParameter("whitelist")); if (!whitelist.endsWith("\n")) whitelist+="\n"; @@ -206,11 +209,13 @@ public class AdminWhitelistRule implements StaplerProxy { } public void setMasterKillSwitch(boolean state) { + final Jenkins jenkins = Jenkins.get(); try { jenkins.checkPermission(Jenkins.RUN_SCRIPTS); - FileUtils.writeStringToFile(getMasterKillSwitchFile(),Boolean.toString(state)); + File f = getMasterKillSwitchFile(jenkins); + FileUtils.writeStringToFile(f, Boolean.toString(state)); // treat the file as the canonical source of information in case write fails - masterKillSwitch = loadMasterKillSwitchFile(); + masterKillSwitch = loadMasterKillSwitchFile(f); } catch (IOException e) { LOGGER.log(WARNING, "Failed to write master kill switch", e); } @@ -221,7 +226,7 @@ public class AdminWhitelistRule implements StaplerProxy { */ @Override public Object getTarget() { - jenkins.checkPermission(Jenkins.RUN_SCRIPTS); + Jenkins.get().checkPermission(Jenkins.RUN_SCRIPTS); return this; } diff --git a/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java b/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java index d21c5a5880d73d3251fed8a4e9c8d6ed14df8ae9..194e83c81397cd53f3b1d032d9d9166ed2adedb5 100644 --- a/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java +++ b/core/src/main/java/jenkins/security/s2m/CallableDirectionChecker.java @@ -22,7 +22,7 @@ import java.util.logging.Logger; * Inspects {@link Callable}s that run on the master. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ @Restricted(NoExternalUse.class) // used implicitly via listener public class CallableDirectionChecker extends RoleChecker { diff --git a/core/src/main/java/jenkins/security/s2m/CallableWhitelist.java b/core/src/main/java/jenkins/security/s2m/CallableWhitelist.java index 50023a0cd071ad04490efc897bb37c0e7716c0f3..27ef63143ecc3813027f79b0fdfc2d75fd21001a 100644 --- a/core/src/main/java/jenkins/security/s2m/CallableWhitelist.java +++ b/core/src/main/java/jenkins/security/s2m/CallableWhitelist.java @@ -18,7 +18,7 @@ import java.util.Collection; * {@link Callable#checkRoles(RoleChecker)} method. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ public abstract class CallableWhitelist implements ExtensionPoint { /** diff --git a/core/src/main/java/jenkins/security/s2m/ConfigFile.java b/core/src/main/java/jenkins/security/s2m/ConfigFile.java index 69d9358eed778ecc29ac6c9f71f548fad17c89f0..99b8c5f97516561a3ae5b22ec4d70e9e83c925fb 100644 --- a/core/src/main/java/jenkins/security/s2m/ConfigFile.java +++ b/core/src/main/java/jenkins/security/s2m/ConfigFile.java @@ -3,6 +3,7 @@ package jenkins.security.s2m; import hudson.CopyOnWrite; import hudson.util.TextFile; import jenkins.model.Jenkins; +import jenkins.util.io.LinesStream; import java.io.BufferedReader; import java.io.File; @@ -26,15 +27,42 @@ abstract class ConfigFile> extends TextFile { protected abstract COL create(); protected abstract COL readOnly(COL base); - public synchronized void load() { + /** + * Loads the configuration from the configuration file. + *

+ * This method is equivalent to {@link #load2()}, except that any + * {@link java.io.IOException} that occurs is wrapped as a + * {@link java.lang.RuntimeException}. + *

+ * This method exists for source compatibility. Users should call + * {@link #load2()} instead. + * @deprecated use {@link #load2()} instead. + */ + @Deprecated + public void load() { + try { + load2(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Loads the configuration from the configuration file. + * @throws IOException if the configuration file could not be read. + * @since 2.111 + */ + public synchronized void load2() throws IOException { COL result = create(); if (exists()) { - for (String line : lines()) { - if (line.startsWith("#")) continue; // comment - T r = parse(line); - if (r != null) - result.add(r); + try (LinesStream stream = linesStream()) { + for (String line : stream) { + if (line.startsWith("#")) continue; // comment + T r = parse(line); + if (r != null) + result.add(r); + } } } @@ -63,7 +91,7 @@ abstract class ConfigFile> extends TextFile { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); write(newContent); - load(); + load2(); } public synchronized void append(String additional) throws IOException { @@ -79,8 +107,9 @@ abstract class ConfigFile> extends TextFile { // load upon the first use if (parsed==null) { synchronized (this) { - if (parsed==null) + if (parsed==null) { load(); + } } } return parsed; diff --git a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java index a35bfbe814ead7e3fdf6821c3a579ff0778fc783..356445b76c91e8f00412c53f25c4d4fdb60eea7b 100644 --- a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java +++ b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java @@ -30,7 +30,7 @@ import hudson.remoting.ChannelBuilder; import jenkins.ReflectiveFilePathFilter; import jenkins.security.ChannelConfigurator; import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import java.io.File; import java.util.logging.Level; @@ -39,7 +39,7 @@ import java.util.logging.Logger; /** * Blocks agents from writing to files on the master by default (and also provide the kill switch.) */ -@Restricted(DoNotUse.class) // impl +@Restricted(NoExternalUse.class) // impl @Extension public class DefaultFilePathFilter extends ChannelConfigurator { /** diff --git a/core/src/main/java/jenkins/security/s2m/MasterKillSwitchConfiguration.java b/core/src/main/java/jenkins/security/s2m/MasterKillSwitchConfiguration.java index 69bb8e34bc1f71b455047939d3ffe2cbd928e2a4..66e20fdd227fc9f39ceed04bf025e6e25c5940ca 100644 --- a/core/src/main/java/jenkins/security/s2m/MasterKillSwitchConfiguration.java +++ b/core/src/main/java/jenkins/security/s2m/MasterKillSwitchConfiguration.java @@ -1,6 +1,8 @@ package jenkins.security.s2m; import hudson.Extension; + +import javax.annotation.Nonnull; import javax.inject.Inject; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; @@ -12,7 +14,7 @@ import org.kohsuke.stapler.StaplerRequest; * Exposes {@link AdminWhitelistRule#masterKillSwitch} to the admin. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ @Extension public class MasterKillSwitchConfiguration extends GlobalConfiguration { @@ -23,7 +25,7 @@ public class MasterKillSwitchConfiguration extends GlobalConfiguration { Jenkins jenkins; @Override - public GlobalConfigurationCategory getCategory() { + public @Nonnull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); } diff --git a/core/src/main/java/jenkins/security/s2m/MasterKillSwitchWarning.java b/core/src/main/java/jenkins/security/s2m/MasterKillSwitchWarning.java index 54164c00c1790a40688ffead26e8c17a3d24e084..ccbb09c6f0a14f391289f01255b1a5a20825177f 100644 --- a/core/src/main/java/jenkins/security/s2m/MasterKillSwitchWarning.java +++ b/core/src/main/java/jenkins/security/s2m/MasterKillSwitchWarning.java @@ -14,7 +14,7 @@ import java.io.IOException; * If {@link AdminWhitelistRule#masterKillSwitch} is on, warn the user. * * @author Kohsuke Kawaguchi - * @since 1.THU + * @since 1.587 / 1.580.1 */ @Extension public class MasterKillSwitchWarning extends AdministrativeMonitor { diff --git a/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java b/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d8f91739522fbd9d6a3d734e8bba179927440e84 --- /dev/null +++ b/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.seed; + +import hudson.ExtensionList; +import hudson.model.User; +import jenkins.security.SecurityListener; +import org.apache.tools.ant.ExtensionPoint; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Listener notified when a user was requested to changed their seed + */ +//TODO remove restriction on the weekly after the security fix +@Restricted(NoExternalUse.class) +public abstract class UserSeedChangeListener extends ExtensionPoint { + private static final Logger LOGGER = Logger.getLogger(SecurityListener.class.getName()); + + /** + * Called after a seed was changed but before the user is saved. + * @param user The target user + */ + public abstract void onUserSeedRenewed(@Nonnull User user); + + /** + * Will notify all the registered listeners about the event + * @param user The target user + */ + public static void fireUserSeedRenewed(@Nonnull User user) { + for (UserSeedChangeListener l : all()) { + try { + l.onUserSeedRenewed(user); + } + catch (Exception e) { + LOGGER.log(Level.WARNING, "Exception caught during onUserSeedRenewed event", e); + } + } + } + + private static List all() { + return ExtensionList.lookup(UserSeedChangeListener.class); + } +} diff --git a/core/src/main/java/jenkins/security/seed/UserSeedProperty.java b/core/src/main/java/jenkins/security/seed/UserSeedProperty.java new file mode 100644 index 0000000000000000000000000000000000000000..6fab2275938e568c3f8e6d62f935f1649ba9f78b --- /dev/null +++ b/core/src/main/java/jenkins/security/seed/UserSeedProperty.java @@ -0,0 +1,154 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.seed; + +import hudson.BulkChange; +import hudson.Extension; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import hudson.util.HttpResponses; +import jenkins.model.Jenkins; +import jenkins.security.LastGrantedAuthoritiesProperty; +import jenkins.util.SystemProperties; +import org.apache.commons.codec.binary.Hex; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.interceptor.RequirePOST; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Objects; + +/** + * The seed stored in this property is used to have a revoke feature on the session + * without having to hack the session management that depends on the application server used to run the instance. + * + * The seed is added to the session when a user just logged in and then for every request, + * before using the session information, we check the seed was not changed in the meantime. + * + * This feature allows the admin to revoke all the sessions that are in the wild without having to keep a list of them. + * + * @see hudson.security.AuthenticationProcessingFilter2 for the addition of seed inside the session + * @see hudson.security.HttpSessionContextIntegrationFilter2 for the seed check from the session before using it + */ +//TODO remove restriction on the weekly after the security fix +@Restricted(NoExternalUse.class) +public class UserSeedProperty extends UserProperty { + /** + * Escape hatch for User seed based revocation feature. + * If we disable the seed, we can still use it to write / store information but not verifying the data using it. + */ + @Restricted(NoExternalUse.class) + public static /* Script Console modifiable */ boolean DISABLE_USER_SEED = SystemProperties.getBoolean(UserSeedProperty.class.getName() + ".disableUserSeed"); + + /** + * Hide the user seed section from the UI to prevent accidental use + */ + @Restricted(NoExternalUse.class) + public static /* Script Console modifiable */ boolean HIDE_USER_SEED_SECTION = SystemProperties.getBoolean(UserSeedProperty.class.getName() + ".hideUserSeedSection"); + + public static final String USER_SESSION_SEED = "_JENKINS_SESSION_SEED"; + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int SEED_NUM_BYTES = 8; + + private String seed; + + private UserSeedProperty() { + this.renewSeedInternal(); + } + + public @Nonnull String getSeed() { + return seed; + } + + public void renewSeed() { + this.renewSeedInternal(); + + UserSeedChangeListener.fireUserSeedRenewed(this.user); + } + + private void renewSeedInternal() { + String currentSeed = this.seed; + String newSeed = currentSeed; + byte[] bytes = new byte[SEED_NUM_BYTES]; + while (Objects.equals(newSeed, currentSeed)) { + RANDOM.nextBytes(bytes); + newSeed = new String(Hex.encodeHex(bytes)); + } + this.seed = newSeed; + } + + @Extension + @Symbol("userSeed") + public static final class DescriptorImpl extends UserPropertyDescriptor { + public @Nonnull String getDisplayName() { + return Messages.UserSeedProperty_DisplayName(); + } + + public UserSeedProperty newInstance(User user) { + return new UserSeedProperty(); + } + + // only for jelly + @Restricted(DoNotUse.class) + public boolean isCurrentUser(@Nonnull User target) { + return Objects.equals(User.current(), target); + } + + @RequirePOST + public synchronized HttpResponse doRenewSessionSeed(@AncestorInPath @Nonnull User u) throws IOException { + u.checkPermission(Jenkins.ADMINISTER); + + if (DISABLE_USER_SEED) { + return HttpResponses.error(404, "User seed feature is disabled"); + } + + try (BulkChange bc = new BulkChange(u)) { + UserSeedProperty p = u.getProperty(UserSeedProperty.class); + p.renewSeed(); + + LastGrantedAuthoritiesProperty lastGranted = u.getProperty(LastGrantedAuthoritiesProperty.class); + if (lastGranted != null) { + lastGranted.invalidate(); + } + + bc.commit(); + } + + return HttpResponses.ok(); + } + + @Override + public boolean isEnabled() { + return !DISABLE_USER_SEED && !HIDE_USER_SEED_SECTION; + } + } +} diff --git a/core/src/main/java/jenkins/security/seed/UserSeedSecurityListener.java b/core/src/main/java/jenkins/security/seed/UserSeedSecurityListener.java new file mode 100644 index 0000000000000000000000000000000000000000..8f4fbf5d549054a782df82132dd0dd3b8e0f5f0b --- /dev/null +++ b/core/src/main/java/jenkins/security/seed/UserSeedSecurityListener.java @@ -0,0 +1,82 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.seed; + +import hudson.Extension; +import hudson.model.User; +import jenkins.security.SecurityListener; +import org.acegisecurity.userdetails.UserDetails; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.Nonnull; +import javax.servlet.http.HttpSession; + +/** + * Inject the user seed inside the session (when there is an existing request) as part of the re-authentication mechanism + * provided by {@link hudson.security.HttpSessionContextIntegrationFilter2} and {@link UserSeedProperty}. + */ +@Restricted(NoExternalUse.class) +@Extension(ordinal = Integer.MAX_VALUE) +public class UserSeedSecurityListener extends SecurityListener { + @Override + protected void loggedIn(@Nonnull String username) { + putUserSeedInSession(username); + } + + @Override + protected void authenticated(@Nonnull UserDetails details) { + putUserSeedInSession(details.getUsername()); + } + + private void putUserSeedInSession(String username) { + StaplerRequest req = Stapler.getCurrentRequest(); + if (req == null) { + // expected case: CLI + // But also HudsonPrivateSecurityRealm because of a redirect from Acegi, the request is not a Stapler one + return; + } + + HttpSession session = req.getSession(false); + if (session == null) { + // expected case: CLI through CLIRegisterer + return; + } + + if (!UserSeedProperty.DISABLE_USER_SEED) { + User user = User.getById(username, true); + + UserSeedProperty userSeed = user.getProperty(UserSeedProperty.class); + if (userSeed == null) { + // if you want to filter out the user seed property, you should consider using the DISABLE_USER_SEED instead + return; + } + String sessionSeed = userSeed.getSeed(); + // normally invalidated before + session.setAttribute(UserSeedProperty.USER_SESSION_SEED, sessionSeed); + } + } +} diff --git a/core/src/main/java/jenkins/security/stapler/DoActionFilter.java b/core/src/main/java/jenkins/security/stapler/DoActionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..288398bcbaeb8c59b7feb0199eb14d64f161bbfc --- /dev/null +++ b/core/src/main/java/jenkins/security/stapler/DoActionFilter.java @@ -0,0 +1,133 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.stapler; + +import hudson.ExtensionList; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Function; +import org.kohsuke.stapler.FunctionList; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.interceptor.InterceptorAnnotation; + +import javax.annotation.Nonnull; +import java.lang.annotation.Annotation; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +@Restricted(NoExternalUse.class) +public class DoActionFilter implements FunctionList.Filter { + private static final Logger LOGGER = Logger.getLogger(DoActionFilter.class.getName()); + + /** + * if a method has "do" as name (not possible in pure Java but doable in Groovy or other JVM languages) + * the new system does not consider it as a web method. + *

+ * Use @WebMethod(name="") or doIndex in such case. + */ + private static final Pattern DO_METHOD_REGEX = Pattern.compile("^do[^a-z].*"); + + public boolean keep(@Nonnull Function m) { + + if (m.getAnnotation(StaplerNotDispatchable.class) != null) { + return false; + } + + if (m.getAnnotation(StaplerDispatchable.class) != null) { + return true; + } + + String methodName = m.getName(); + String signature = m.getSignature(); + + // check whitelist + ExtensionList whitelistProviders = ExtensionList.lookup(RoutingDecisionProvider.class); + if (whitelistProviders.size() > 0) { + for (RoutingDecisionProvider provider : whitelistProviders) { + RoutingDecisionProvider.Decision methodDecision = provider.decide(signature); + if (methodDecision == RoutingDecisionProvider.Decision.ACCEPTED) { + LOGGER.log(Level.CONFIG, "Action " + signature + " is acceptable because it is whitelisted by " + provider); + return true; + } + if (methodDecision == RoutingDecisionProvider.Decision.REJECTED) { + LOGGER.log(Level.CONFIG, "Action " + signature + " is not acceptable because it is blacklisted by " + provider); + return false; + } + } + } + + if (methodName.equals("doDynamic")) { + // reject doDynamic because it's treated separately by Stapler. + return false; + } + + for (Annotation a : m.getAnnotations()) { + if (WebMethodConstants.WEB_METHOD_ANNOTATION_NAMES.contains(a.annotationType().getName())) { + return true; + } + if (a.annotationType().getAnnotation(InterceptorAnnotation.class) != null) { + // This is a Stapler interceptor annotation like RequirePOST or JsonResponse + return true; + } + } + + // there is rarely more than two annotations in a method signature + for (Annotation[] perParameterAnnotation : m.getParameterAnnotations()) { + for (Annotation annotation : perParameterAnnotation) { + if (WebMethodConstants.WEB_METHOD_PARAMETER_ANNOTATION_NAMES.contains(annotation.annotationType().getName())) { + return true; + } + } + } + + if (!DO_METHOD_REGEX.matcher(methodName).matches()) { + return false; + } + + // after the method name check to avoid allowing methods that are meant to be used by routable ones + // normally they should be private in such case + for (Class parameterType : m.getParameterTypes()) { + if (WebMethodConstants.WEB_METHOD_PARAMETERS_NAMES.contains(parameterType.getName())) { + return true; + } + } + + Class returnType = m.getReturnType(); + if (HttpResponse.class.isAssignableFrom(returnType)) { + return true; + } + + // as HttpResponseException inherits from RuntimeException, + // there is no requirement for the developer to explicitly checks it. + Class[] checkedExceptionTypes = m.getCheckedExceptionTypes(); + for (Class checkedExceptionType : checkedExceptionTypes) { + if (HttpResponse.class.isAssignableFrom(checkedExceptionType)) { + return true; + } + } + + return false; + } +} diff --git a/test/src/test/groovy/hudson/GroovyTest.groovy b/core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java similarity index 74% rename from test/src/test/groovy/hudson/GroovyTest.groovy rename to core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java index e512cf56ef9567647722cb0768bc408556e77c21..c7cab91839b76bbaa48843d11b1e0869fc64efe4 100644 --- a/test/src/test/groovy/hudson/GroovyTest.groovy +++ b/core/src/main/java/jenkins/security/stapler/RoutingDecisionProvider.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. + * Copyright (c) 2018, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,25 +21,18 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson +package jenkins.security.stapler; -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule +import hudson.ExtensionPoint; -/** - * First groovy test! - * - * @author Kohsuke Kawaguchi - */ -public class GroovyTest { +import javax.annotation.Nonnull; - @Rule - public JenkinsRule j = new JenkinsRule() - - @Test - void test() { - def wc = j.createWebClient(); - wc.goTo(""); +public abstract class RoutingDecisionProvider implements ExtensionPoint { + enum Decision { + ACCEPTED, + REJECTED, + UNKNOWN } + + @Nonnull public abstract Decision decide(@Nonnull String signature); } diff --git a/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java b/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java new file mode 100644 index 0000000000000000000000000000000000000000..bdc329775f6dbc328da9825a8baeeeffbeb2dfdd --- /dev/null +++ b/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.stapler; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Function; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.event.FilteredDoActionTriggerListener; +import org.kohsuke.stapler.event.FilteredFieldTriggerListener; +import org.kohsuke.stapler.event.FilteredGetterTriggerListener; +import org.kohsuke.stapler.lang.FieldRef; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Log a warning message when a "getter" or "doAction" function that was filtered out by SECURITY-400 new rules + */ +@Restricted(NoExternalUse.class) +public class StaplerFilteredActionListener implements FilteredDoActionTriggerListener, FilteredGetterTriggerListener, FilteredFieldTriggerListener { + private static final Logger LOGGER = Logger.getLogger(StaplerFilteredActionListener.class.getName()); + + private static final String LOG_MESSAGE = "New Stapler routing rules result in the URL \"{0}\" no longer being allowed. " + + "If you consider it safe to use, add the following to the whitelist: \"{1}\". " + + "Learn more: https://jenkins.io/redirect/stapler-routing"; + + @Override + public boolean onDoActionTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node) { + LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{ + req.getPathInfo(), + f.getSignature() + }); + return false; + } + + @Override + public boolean onGetterTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node, String expression) { + LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{ + req.getPathInfo(), + f.getSignature() + }); + return false; + } + + @Override + public boolean onFieldTrigger(FieldRef f, StaplerRequest req, StaplerResponse staplerResponse, Object node, String expression) { + LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{ + req.getPathInfo(), + f.getSignature() + }); + return false; + } +} diff --git a/core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java b/core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..cbb58760154073535915b02ce9e22ce9d7d8c322 --- /dev/null +++ b/core/src/main/java/jenkins/security/stapler/StaticRoutingDecisionProvider.java @@ -0,0 +1,266 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.stapler; + +import com.google.common.annotations.VisibleForTesting; +import hudson.BulkChange; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.Saveable; +import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Function; +import org.kohsuke.stapler.WebApp; +import org.kohsuke.stapler.lang.FieldRef; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Fill the list of getter methods that are whitelisted for Stapler + * Each item in the set are formatted to correspond exactly to what {@link Function#getDisplayName()} returns + */ +@Restricted(NoExternalUse.class) +@Extension +public class StaticRoutingDecisionProvider extends RoutingDecisionProvider implements Saveable { + private static final Logger LOGGER = Logger.getLogger(StaticRoutingDecisionProvider.class.getName()); + + private Set whitelistSignaturesFromFixedList; + private Set whitelistSignaturesFromUserControlledList; + + private Set blacklistSignaturesFromFixedList; + private Set blacklistSignaturesFromUserControlledList; + + public StaticRoutingDecisionProvider() { + reload(); + } + + /** + * Return the singleton instance of this class, typically for script console use + */ + public static StaticRoutingDecisionProvider get() { + return ExtensionList.lookupSingleton(StaticRoutingDecisionProvider.class); + } + + /** + * @see Function#getSignature() + * @see FieldRef#getSignature() + */ + @Nonnull + public synchronized Decision decide(@Nonnull String signature) { + if (whitelistSignaturesFromFixedList == null || whitelistSignaturesFromUserControlledList == null || + blacklistSignaturesFromFixedList == null || blacklistSignaturesFromUserControlledList == null) { + reload(); + } + + LOGGER.log(Level.CONFIG, "Checking whitelist for " + signature); + + // priority to blacklist + if (blacklistSignaturesFromFixedList.contains(signature) || blacklistSignaturesFromUserControlledList.contains(signature)) { + return Decision.REJECTED; + } + + if (whitelistSignaturesFromFixedList.contains(signature) || whitelistSignaturesFromUserControlledList.contains(signature)) { + return Decision.ACCEPTED; + } + + return Decision.UNKNOWN; + } + + public synchronized void reload() { + reloadFromDefault(); + reloadFromUserControlledList(); + + resetMetaClassCache(); + } + + @VisibleForTesting + synchronized void resetAndSave(){ + this.whitelistSignaturesFromFixedList = new HashSet<>(); + this.whitelistSignaturesFromUserControlledList = new HashSet<>(); + this.blacklistSignaturesFromFixedList = new HashSet<>(); + this.blacklistSignaturesFromUserControlledList = new HashSet<>(); + + this.save(); + } + + private void resetMetaClassCache() { + // to allow the change to be effective, i.e. rebuild the MetaClass using the new whitelist + WebApp.get(Jenkins.get().servletContext).clearMetaClassCache(); + } + + private synchronized void reloadFromDefault() { + try (InputStream is = StaticRoutingDecisionProvider.class.getResourceAsStream("default-whitelist.txt")) { + whitelistSignaturesFromFixedList = new HashSet<>(); + blacklistSignaturesFromFixedList = new HashSet<>(); + + parseFileIntoList( + IOUtils.readLines(is, StandardCharsets.UTF_8), + whitelistSignaturesFromFixedList, + blacklistSignaturesFromFixedList + ); + } catch (IOException e) { + throw new ExceptionInInitializerError(e); + } + + LOGGER.log(Level.FINE, "Found {0} getter in the standard whitelist", whitelistSignaturesFromFixedList.size()); + } + + public synchronized StaticRoutingDecisionProvider add(@Nonnull String signature) { + if (this.whitelistSignaturesFromUserControlledList.add(signature)) { + LOGGER.log(Level.INFO, "Signature [{0}] added to the whitelist", signature); + save(); + resetMetaClassCache(); + } else { + LOGGER.log(Level.INFO, "Signature [{0}] was already present in the whitelist", signature); + } + return this; + } + + public synchronized StaticRoutingDecisionProvider addBlacklistSignature(@Nonnull String signature) { + if (this.blacklistSignaturesFromUserControlledList.add(signature)) { + LOGGER.log(Level.INFO, "Signature [{0}] added to the blacklist", signature); + save(); + resetMetaClassCache(); + } else { + LOGGER.log(Level.INFO, "Signature [{0}] was already present in the blacklist", signature); + } + return this; + } + + public synchronized StaticRoutingDecisionProvider remove(@Nonnull String signature) { + if (this.whitelistSignaturesFromUserControlledList.remove(signature)) { + LOGGER.log(Level.INFO, "Signature [{0}] removed from the whitelist", signature); + save(); + resetMetaClassCache(); + } else { + LOGGER.log(Level.INFO, "Signature [{0}] was not present in the whitelist", signature); + } + return this; + } + + public synchronized StaticRoutingDecisionProvider removeBlacklistSignature(@Nonnull String signature) { + if (this.blacklistSignaturesFromUserControlledList.remove(signature)) { + LOGGER.log(Level.INFO, "Signature [{0}] removed from the blacklist", signature); + save(); + resetMetaClassCache(); + } else { + LOGGER.log(Level.INFO, "Signature [{0}] was not present in the blacklist", signature); + } + return this; + } + + /** + * Saves the configuration info to the disk. + */ + public synchronized void save() { + if (BulkChange.contains(this)) { + return; + } + + File file = getConfigFile(); + try { + List allSignatures = new ArrayList<>(whitelistSignaturesFromUserControlledList); + blacklistSignaturesFromUserControlledList.stream() + .map(signature -> "!" + signature) + .forEach(allSignatures::add); + + FileUtils.writeLines(file, allSignatures); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to save " + file.getAbsolutePath(), e); + } + } + + /** + * Loads the data from the disk into this object. + * + *

+ * The constructor of the derived class must call this method. + * (If we do that in the base class, the derived class won't + * get a chance to set default values.) + */ + private synchronized void reloadFromUserControlledList() { + File file = getConfigFile(); + if (!file.exists()) { + if ((whitelistSignaturesFromUserControlledList != null && whitelistSignaturesFromUserControlledList.isEmpty()) || + (blacklistSignaturesFromUserControlledList != null && blacklistSignaturesFromUserControlledList.isEmpty())) { + LOGGER.log(Level.INFO, "No whitelist source file found at " + file + " so resetting user-controlled whitelist"); + } + whitelistSignaturesFromUserControlledList = new HashSet<>(); + blacklistSignaturesFromUserControlledList = new HashSet<>(); + return; + } + + LOGGER.log(Level.INFO, "Whitelist source file found at " + file); + + try { + whitelistSignaturesFromUserControlledList = new HashSet<>(); + blacklistSignaturesFromUserControlledList = new HashSet<>(); + + parseFileIntoList( + FileUtils.readLines(file, StandardCharsets.UTF_8), + whitelistSignaturesFromUserControlledList, + blacklistSignaturesFromUserControlledList + ); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to load " + file.getAbsolutePath(), e); + } + } + + private File getConfigFile() { + return new File(WHITELIST_PATH == null ? new File(Jenkins.get().getRootDir(), "stapler-whitelist.txt").toString() : WHITELIST_PATH); + } + + private void parseFileIntoList(List lines, Set whitelist, Set blacklist){ + lines.stream() + .filter(line -> !line.matches("#.*|\\s*")) + .forEach(line -> { + if (line.startsWith("!")) { + String withoutExclamation = line.substring(1); + if (!withoutExclamation.isEmpty()) { + blacklist.add(withoutExclamation); + } + } else { + whitelist.add(line); + } + }); + } + + /** Allow script console access */ + public static String WHITELIST_PATH = SystemProperties.getString(StaticRoutingDecisionProvider.class.getName() + ".whitelist"); + +} diff --git a/core/src/main/java/jenkins/security/stapler/TypedFilter.java b/core/src/main/java/jenkins/security/stapler/TypedFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..6f9bdb4019d887e932e48b0e69fd19f435f841a5 --- /dev/null +++ b/core/src/main/java/jenkins/security/stapler/TypedFilter.java @@ -0,0 +1,276 @@ +package jenkins.security.stapler; + +import hudson.ExtensionList; +import jenkins.util.SystemProperties; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Function; +import org.kohsuke.stapler.FunctionList; +import org.kohsuke.stapler.StaplerFallback; +import org.kohsuke.stapler.StaplerOverridable; +import org.kohsuke.stapler.StaplerProxy; +import org.kohsuke.stapler.WebApp; +import org.kohsuke.stapler.interceptor.InterceptorAnnotation; +import org.kohsuke.stapler.lang.FieldRef; + +import javax.annotation.Nonnull; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Restricted(NoExternalUse.class) +public class TypedFilter implements FieldRef.Filter, FunctionList.Filter { + private static final Logger LOGGER = Logger.getLogger(TypedFilter.class.getName()); + + private static final Map, Boolean> staplerCache = new HashMap<>(); + + private boolean isClassAcceptable(Class clazz) { + if (clazz.isArray()) { + // special case to allow klass.isArray() dispatcher + Class elementClazz = clazz.getComponentType(); + // does not seem possible to fall in an infinite loop since array cannot be recursively defined + if (isClassAcceptable(elementClazz)) { + LOGGER.log(Level.FINE, + "Class {0} is acceptable because it is an Array of acceptable elements {1}", + new Object[]{clazz.getName(), elementClazz.getName()} + ); + return true; + } else { + LOGGER.log(Level.FINE, + "Class {0} is not acceptable because it is an Array of non-acceptable elements {1}", + new Object[]{clazz.getName(), elementClazz.getName()} + ); + return false; + } + } + return SKIP_TYPE_CHECK || isStaplerRelevantCached(clazz); + } + + private static boolean isStaplerRelevantCached(@Nonnull Class clazz) { + if (staplerCache.containsKey(clazz)) { + return staplerCache.get(clazz); + } + boolean ret = isStaplerRelevant(clazz); + + staplerCache.put(clazz, ret); + return ret; + } + + @Restricted(NoExternalUse.class) + public static boolean isStaplerRelevant(@Nonnull Class clazz) { + return isSpecificClassStaplerRelevant(clazz) || isSuperTypesStaplerRelevant(clazz); + } + + private static boolean isSuperTypesStaplerRelevant(@Nonnull Class clazz) { + Class superclass = clazz.getSuperclass(); + if (superclass != null && isStaplerRelevantCached(superclass)) { + return true; + } + for (Class interfaceClass : clazz.getInterfaces()) { + if (isStaplerRelevantCached(interfaceClass)) { + return true; + } + } + return false; + } + + private static boolean isSpecificClassStaplerRelevant(@Nonnull Class clazz) { + if (clazz.isAnnotationPresent(StaplerAccessibleType.class)) { + return true; + } + + // Classes implementing these Stapler types can be considered routable + if (StaplerProxy.class.isAssignableFrom(clazz)) { + return true; + } + if (StaplerFallback.class.isAssignableFrom(clazz)) { + return true; + } + if (StaplerOverridable.class.isAssignableFrom(clazz)) { + return true; + } + + for (Method m : clazz.getMethods()) { + if (isRoutableMethod(m)) { + return true; + } + } + + return false; + } + + private static boolean isRoutableMethod(@Nonnull Method m) { + for (Annotation a : m.getDeclaredAnnotations()) { + if (WebMethodConstants.WEB_METHOD_ANNOTATION_NAMES.contains(a.annotationType().getName())) { + return true; + } + if (a.annotationType().isAnnotationPresent(InterceptorAnnotation.class)) { + // This is a Stapler interceptor annotation like RequirePOST or JsonResponse + return true; + } + } + + for (Annotation[] set : m.getParameterAnnotations()) { + for (Annotation a : set) { + if (WebMethodConstants.WEB_METHOD_PARAMETER_ANNOTATION_NAMES.contains(a.annotationType().getName())) { + return true; + } + } + } + + for (Class parameterType : m.getParameterTypes()) { + if (WebMethodConstants.WEB_METHOD_PARAMETERS_NAMES.contains(parameterType.getName())) { + return true; + } + } + + return WebApp.getCurrent().getFilterForDoActions().keep(new Function.InstanceFunction(m)); + } + + @Override + public boolean keep(@Nonnull FieldRef fieldRef) { + + if (fieldRef.getAnnotation(StaplerNotDispatchable.class) != null) { + // explicitly marked as an invalid field + return false; + } + + if (fieldRef.getAnnotation(StaplerDispatchable.class) != null) { + // explicitly marked as a valid field + return true; + } + + String signature = fieldRef.getSignature(); + + // check whitelist + ExtensionList decisionProviders = ExtensionList.lookup(RoutingDecisionProvider.class); + if (decisionProviders.size() > 0) { + for (RoutingDecisionProvider provider : decisionProviders) { + RoutingDecisionProvider.Decision fieldDecision = provider.decide(signature); + if (fieldDecision == RoutingDecisionProvider.Decision.ACCEPTED) { + LOGGER.log(Level.CONFIG, "Field {0} is acceptable because it is whitelisted by {1}", new Object[]{signature, provider}); + return true; + } + if (fieldDecision == RoutingDecisionProvider.Decision.REJECTED) { + LOGGER.log(Level.CONFIG, "Field {0} is not acceptable because it is blacklisted by {1}", new Object[]{signature, provider}); + return false; + } + Class type = fieldRef.getReturnType(); + if (type != null) { + String typeSignature = "class " + type.getCanonicalName(); + RoutingDecisionProvider.Decision fieldTypeDecision = provider.decide(typeSignature); + if (fieldTypeDecision == RoutingDecisionProvider.Decision.ACCEPTED) { + LOGGER.log(Level.CONFIG, "Field {0} is acceptable because its type is whitelisted by {1}", new Object[]{signature, provider}); + return true; + } + if (fieldTypeDecision == RoutingDecisionProvider.Decision.REJECTED) { + LOGGER.log(Level.CONFIG, "Field {0} is not acceptable because its type is blacklisted by {1}", new Object[]{signature, provider}); + return false; + } + } + } + } + + if (PROHIBIT_STATIC_ACCESS && fieldRef.isStatic()) { + // unless whitelisted or marked as routable, reject static fields + return false; + } + + + Class returnType = fieldRef.getReturnType(); + + boolean isOk = isClassAcceptable(returnType); + LOGGER.log(Level.FINE, "Field analyzed: {0} => {1}", new Object[]{fieldRef.getName(), isOk}); + return isOk; + } + + @Override + public boolean keep(@Nonnull Function function) { + + if (function.getAnnotation(StaplerNotDispatchable.class) != null) { + // explicitly marked as an invalid getter + return false; + } + + if (function.getAnnotation(StaplerDispatchable.class) != null) { + // explicitly marked as a valid getter + return true; + } + + String signature = function.getSignature(); + + // check whitelist + ExtensionList decision = ExtensionList.lookup(RoutingDecisionProvider.class); + if (decision.size() > 0) { + for (RoutingDecisionProvider provider : decision) { + RoutingDecisionProvider.Decision methodDecision = provider.decide(signature); + if (methodDecision == RoutingDecisionProvider.Decision.ACCEPTED) { + LOGGER.log(Level.CONFIG, "Function {0} is acceptable because it is whitelisted by {1}", new Object[]{signature, provider}); + return true; + } + if (methodDecision == RoutingDecisionProvider.Decision.REJECTED) { + LOGGER.log(Level.CONFIG, "Function {0} is not acceptable because it is blacklisted by {1}", new Object[]{signature, provider}); + return false; + } + + Class type = function.getReturnType(); + if (type != null) { + String typeSignature = "class " + type.getCanonicalName(); + RoutingDecisionProvider.Decision returnTypeDecision = provider.decide(typeSignature); + if (returnTypeDecision == RoutingDecisionProvider.Decision.ACCEPTED) { + LOGGER.log(Level.CONFIG, "Function {0} is acceptable because its type is whitelisted by {1}", new Object[]{signature, provider}); + return true; + } + if (returnTypeDecision == RoutingDecisionProvider.Decision.REJECTED) { + LOGGER.log(Level.CONFIG, "Function {0} is not acceptable because its type is blacklisted by {1}", new Object[]{signature, provider}); + return false; + } + } + } + } + + if (PROHIBIT_STATIC_ACCESS && function.isStatic()) { + // unless whitelisted or marked as routable, reject static methods + return false; + } + + if (function.getName().equals("getDynamic")) { + Class[] parameterTypes = function.getParameterTypes(); + if (parameterTypes.length > 0 && parameterTypes[0] == String.class) { + // While this is more general than what Stapler can invoke on these types, + // The above is the only criterion for Stapler to attempt dispatch. + // Therefore prohibit this as a regular getter. + return false; + } + } + + if (function.getName().equals("getStaplerFallback") && function.getParameterTypes().length == 0) { + // A parameter-less #getStaplerFallback() implements special fallback behavior for the + // StaplerFallback interface. We do not check for the presence of the interface on the current + // class, or the return type, as that could change since the implementing component was last built. + return false; + } + + if (function.getName().equals("getTarget") && function.getParameterTypes().length == 0) { + // A parameter-less #getTarget() implements special redirection behavior for the + // StaplerProxy interface. We do not check for the presence of the interface on the current + // class, or the return type, as that could change since the implementing component was last built. + return false; + } + + Class returnType = function.getReturnType(); + + boolean isOk = isClassAcceptable(returnType); + LOGGER.log(Level.FINE, "Function analyzed: {0} => {1}", new Object[]{signature, isOk}); + return isOk; + } + + @Restricted(NoExternalUse.class) + public static boolean SKIP_TYPE_CHECK = SystemProperties.getBoolean(TypedFilter.class.getName() + ".skipTypeCheck"); + + @Restricted(NoExternalUse.class) + public static boolean PROHIBIT_STATIC_ACCESS = SystemProperties.getBoolean(TypedFilter.class.getName() + ".prohibitStaticAccess", true); +} diff --git a/core/src/main/java/jenkins/security/stapler/WebMethodConstants.java b/core/src/main/java/jenkins/security/stapler/WebMethodConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..d8f678e4b072b3747bece29870f4deff9d76980e --- /dev/null +++ b/core/src/main/java/jenkins/security/stapler/WebMethodConstants.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security.stapler; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.AncestorInPath; +import org.kohsuke.stapler.Header; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.WebMethod; +import org.kohsuke.stapler.bind.JavaScriptMethod; +import org.kohsuke.stapler.json.JsonBody; +import org.kohsuke.stapler.json.SubmittedForm; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Restricted(NoExternalUse.class) +final class WebMethodConstants { + /** + * If a method has at least one of those parameters, it is considered as an implicit web method + */ + private static final List> WEB_METHOD_PARAMETERS = Collections.unmodifiableList(Arrays.asList( + StaplerRequest.class, + HttpServletRequest.class, + StaplerResponse.class, + HttpServletResponse.class + )); + + static final Set WEB_METHOD_PARAMETERS_NAMES = Collections.unmodifiableSet( + WEB_METHOD_PARAMETERS.stream() + .map(Class::getName) + .collect(Collectors.toSet()) + ); + + /** + * If a method is annotated with one of those annotations, + * the method is considered as an explicit web method + */ + static final List> WEB_METHOD_ANNOTATIONS = Collections.singletonList( + WebMethod.class + // plus every annotation that's annotated with InterceptorAnnotation + // JavaScriptMethod.class not taken here because it's a special case + ); + + static final Set WEB_METHOD_ANNOTATION_NAMES; + static { + Set webMethodAnnotationNames = WEB_METHOD_ANNOTATIONS.stream() + .map(Class::getName) + .collect(Collectors.toSet()); + webMethodAnnotationNames.add(JavaScriptMethod.class.getName()); + WEB_METHOD_ANNOTATION_NAMES = Collections.unmodifiableSet(webMethodAnnotationNames); + } + + /** + * If at least one parameter of the method is annotated with one of those annotations, + * the method is considered as an implicit web method + */ + private static final List> WEB_METHOD_PARAMETER_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList( + QueryParameter.class, + AncestorInPath.class, + Header.class, + JsonBody.class, + SubmittedForm.class + )); + + static final Set WEB_METHOD_PARAMETER_ANNOTATION_NAMES = Collections.unmodifiableSet( + WEB_METHOD_PARAMETER_ANNOTATIONS.stream() + .map(Class::getName) + .collect(Collectors.toSet()) + ); +} diff --git a/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java b/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java index 127f8b7438870d4f615899ea6472d98b993b281c..6edbe924da93181aee3262799945bb0b2d3dc75d 100644 --- a/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java +++ b/core/src/main/java/jenkins/slaves/DefaultJnlpSlaveReceiver.java @@ -56,7 +56,7 @@ public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver { @Override public boolean owns(String clientName) { - Computer computer = Jenkins.getInstance().getComputer(clientName); + Computer computer = Jenkins.get().getComputer(clientName); return computer != null; } @@ -83,7 +83,7 @@ public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver { @Override public void afterProperties(@NonNull JnlpConnectionState event) { String clientName = event.getProperty(JnlpConnectionState.CLIENT_NAME_KEY); - SlaveComputer computer = (SlaveComputer) Jenkins.getInstance().getComputer(clientName); + SlaveComputer computer = (SlaveComputer) Jenkins.get().getComputer(clientName); if (computer == null) { event.reject(new ConnectionRefusalException(String.format("%s is not a JNLP agent", clientName))); return; diff --git a/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java b/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..b2b5843587b18a326984504e87331339357f0cfc --- /dev/null +++ b/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java @@ -0,0 +1,94 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.slaves; + +import hudson.Extension; +import hudson.model.AdministrativeMonitor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.annotation.CheckForNull; +import jenkins.AgentProtocol; +import jenkins.model.Jenkins; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + + +/** + * Monitors enabled protocols and warns if an {@link AgentProtocol} is deprecated. + * + * @author Oleg Nenashev + * @since 2.75 + * @see AgentProtocol + */ +@Extension +@Symbol("deprecatedAgentProtocol") +@Restricted(NoExternalUse.class) +public class DeprecatedAgentProtocolMonitor extends AdministrativeMonitor { + + public DeprecatedAgentProtocolMonitor() { + super(); + } + + @Override + public String getDisplayName() { + return Messages.DeprecatedAgentProtocolMonitor_displayName(); + } + + @Override + public boolean isActivated() { + final Set agentProtocols = Jenkins.get().getAgentProtocols(); + for (String name : agentProtocols) { + AgentProtocol pr = AgentProtocol.of(name); + if (pr != null && pr.isDeprecated()) { + return true; + } + } + return false; + } + + @Restricted(NoExternalUse.class) + public String getDeprecatedProtocols() { + String res = getDeprecatedProtocolsString(); + return res != null ? res : "N/A"; + } + + @CheckForNull + public static String getDeprecatedProtocolsString() { + final List deprecatedProtocols = new ArrayList<>(); + final Set agentProtocols = Jenkins.get().getAgentProtocols(); + for (String name : agentProtocols) { + AgentProtocol pr = AgentProtocol.of(name); + if (pr != null && pr.isDeprecated()) { + deprecatedProtocols.add(name); + } + } + if (deprecatedProtocols.isEmpty()) { + return null; + } + return StringUtils.join(deprecatedProtocols, ','); + } +} diff --git a/core/src/main/java/jenkins/slaves/EncryptedSlaveAgentJnlpFile.java b/core/src/main/java/jenkins/slaves/EncryptedSlaveAgentJnlpFile.java index 9d152a68f0fb886d99150a386d5965a90901d70c..661d410cadfa59c28e00025df47f4ac9a9f23335 100644 --- a/core/src/main/java/jenkins/slaves/EncryptedSlaveAgentJnlpFile.java +++ b/core/src/main/java/jenkins/slaves/EncryptedSlaveAgentJnlpFile.java @@ -4,12 +4,12 @@ import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.slaves.SlaveComputer; import hudson.util.Secret; + import hudson.Util; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.ResponseImpl; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.compression.FilterServletOutputStream; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -18,12 +18,15 @@ import javax.crypto.spec.SecretKeySpec; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponseWrapper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.security.GeneralSecurityException; import java.security.SecureRandom; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Serves the JNLP file. @@ -35,6 +38,9 @@ import java.security.SecureRandom; * @since 1.560 */ public class EncryptedSlaveAgentJnlpFile implements HttpResponse { + + private static final Logger LOG = Logger.getLogger(EncryptedSlaveAgentJnlpFile.class.getName()); + /** * The object that owns the Jelly view that renders JNLP file. * This is typically a {@link SlaveComputer} and if so we'll use {@link SlaveComputer#getJnlpMac()} @@ -64,13 +70,13 @@ public class EncryptedSlaveAgentJnlpFile implements HttpResponse { } @Override - public void generateResponse(StaplerRequest req, StaplerResponse res, Object node) throws IOException, ServletException { + public void generateResponse(StaplerRequest req, final StaplerResponse res, Object node) throws IOException, ServletException { RequestDispatcher view = req.getView(it, viewName); if ("true".equals(req.getParameter("encrypt"))) { - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final CapturingServletOutputStream csos = new CapturingServletOutputStream(); StaplerResponse temp = new ResponseImpl(req.getStapler(), new HttpServletResponseWrapper(res) { @Override public ServletOutputStream getOutputStream() throws IOException { - return new FilterServletOutputStream(baos); + return csos; } @Override public PrintWriter getWriter() throws IOException { throw new IllegalStateException(); @@ -92,7 +98,7 @@ public class EncryptedSlaveAgentJnlpFile implements HttpResponse { try { Cipher c = Secret.getCipher("AES/CFB8/NoPadding"); c.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); - encrypted = c.doFinal(baos.toByteArray()); + encrypted = c.doFinal(csos.getBytes()); } catch (GeneralSecurityException x) { throw new IOException(x); } @@ -104,4 +110,52 @@ public class EncryptedSlaveAgentJnlpFile implements HttpResponse { view.forward(req, res); } } + + + /** + * A {@link ServletOutputStream} that captures all the data rather than writing to a client. + */ + private static class CapturingServletOutputStream extends ServletOutputStream { + + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + // we are always ready to write so we just call once to say we are ready. + try { + // should we do this on a separate thread to avoid deadlocks? + writeListener.onWritePossible(); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to notify WriteListener.onWritePossible", e); + } + } + + @Override + public void write(int b) throws IOException { + baos.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + baos.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + baos.write(b, off, len); + } + + /** + * Get the data that has been written to this ServletOutputStream. + * @return the data that has been written to this ServletOutputStream. + */ + byte[] getBytes() { + return baos.toByteArray(); + } + } } diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java index e351b93945ed07ad94503def59eb75e7753a44a3..69720d9ccef8a93eafeec51b7f4faaebb21996b1 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java @@ -7,15 +7,12 @@ import hudson.model.Computer; import java.io.IOException; import java.net.Socket; import java.util.Collections; -import java.util.HashMap; import java.util.logging.Logger; -import javax.annotation.Nonnull; import javax.inject.Inject; import jenkins.AgentProtocol; import jenkins.model.Jenkins; import jenkins.security.HMACConfidentialKey; import org.jenkinsci.Symbol; -import org.jenkinsci.remoting.engine.JnlpClientDatabase; import org.jenkinsci.remoting.engine.JnlpConnectionState; import org.jenkinsci.remoting.engine.JnlpProtocol1Handler; @@ -31,11 +28,11 @@ import org.jenkinsci.remoting.engine.JnlpProtocol1Handler; * *

* We do this by computing HMAC of the agent name. - * This code is sent to the agent inside the .jnlp file + * This code is sent to the agent inside the {@code .jnlp} file * (this file itself is protected by HTTP form-based authentication that * we use everywhere else in Jenkins), and the agent sends this * token back when it connects to the master. - * Unauthorized agents can't access the protected .jnlp file, + * Unauthorized agents can't access the protected {@code .jnlp} file, * so it can't impersonate a valid agent. * *

@@ -76,7 +73,12 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol { */ @Override public boolean isOptIn() { - return OPT_IN; + return true; + } + + @Override + public boolean isDeprecated() { + return true; } @Override @@ -100,13 +102,4 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol { } - /** - * A/B test turning off this protocol by default. - */ - private static final boolean OPT_IN; - - static { - byte hash = Util.fromHexString(Jenkins.getInstance().getLegacyInstanceId())[0]; - OPT_IN = (hash % 10) == 0; - } } diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java index 66bc07dc9d72992701ab59889da570498f7002ea..c96d353c8b943487a741683409437aa4c1aec158 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java @@ -6,12 +6,9 @@ import hudson.model.Computer; import java.io.IOException; import java.net.Socket; import java.util.Collections; -import java.util.HashMap; -import javax.annotation.Nonnull; import javax.inject.Inject; import jenkins.AgentProtocol; import org.jenkinsci.Symbol; -import org.jenkinsci.remoting.engine.JnlpClientDatabase; import org.jenkinsci.remoting.engine.JnlpConnectionState; import org.jenkinsci.remoting.engine.JnlpProtocol2Handler; @@ -50,7 +47,12 @@ public class JnlpSlaveAgentProtocol2 extends AgentProtocol { */ @Override public boolean isOptIn() { - return false; + return true; + } + + @Override + public boolean isDeprecated() { + return true; } /** diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java index 50779ee9da4085bb1d527148f202ddfde90a2376..fcc4e240e90b4320303c32f420a15adf3db6da24 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java @@ -8,13 +8,11 @@ import hudson.model.Computer; import java.io.IOException; import java.net.Socket; import java.util.Collections; -import java.util.HashMap; -import javax.annotation.Nonnull; import javax.inject.Inject; import jenkins.AgentProtocol; import jenkins.model.Jenkins; import jenkins.util.SystemProperties; -import org.jenkinsci.remoting.engine.JnlpClientDatabase; +import org.jenkinsci.Symbol; import org.jenkinsci.remoting.engine.JnlpConnectionState; import org.jenkinsci.remoting.engine.JnlpProtocol3Handler; import org.kohsuke.accmod.Restricted; @@ -25,10 +23,11 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; * *

@see {@link org.jenkinsci.remoting.engine.JnlpProtocol3Handler} for more details. * - * @since 1.XXX + * @since 1.653 */ @Deprecated @Extension +@Symbol("jnlp3") public class JnlpSlaveAgentProtocol3 extends AgentProtocol { private NioChannelSelector hub; @@ -46,15 +45,12 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { */ @Override public boolean isOptIn() { - return !ENABLED; + return true ; } @Override public String getName() { - // we only want to force the protocol off for users that have explicitly banned it via system property - // everyone on the A/B test will just have the opt-in flag toggled - // TODO strip all this out and hardcode OptIn==TRUE once JENKINS-36871 is merged - return forceEnabled != Boolean.FALSE ? handler.getName() : null; + return handler.isEnabled() ? handler.getName() : null; } /** @@ -65,6 +61,11 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { return Messages.JnlpSlaveAgentProtocol3_displayName(); } + @Override + public boolean isDeprecated() { + return true; + } + @Override public void handle(Socket socket) throws IOException, InterruptedException { handler.handle(socket, @@ -72,26 +73,4 @@ public class JnlpSlaveAgentProtocol3 extends AgentProtocol { ExtensionList.lookup(JnlpAgentReceiver.class)); } - /** - * Flag to control the activation of JNLP3 protocol. - * - *

- * Once this will be on by default, the flag and this field will disappear. The system property is - * an escape hatch for those who hit any issues and those who are trying this out. - */ - @Restricted(NoExternalUse.class) - @SuppressFBWarnings(value = "MS_SHOULD_BE_REFACTORED_TO_BE_FINAL", - justification = "Part of the administrative API for System Groovy scripts.") - public static boolean ENABLED; - private static final Boolean forceEnabled; - - static { - forceEnabled = SystemProperties.optBoolean(JnlpSlaveAgentProtocol3.class.getName() + ".enabled"); - if (forceEnabled != null) { - ENABLED = forceEnabled; - } else { - byte hash = Util.fromHexString(Jenkins.getActiveInstance().getLegacyInstanceId())[0]; - ENABLED = (hash % 10) == 0; - } - } } diff --git a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java index cfa4ac8de73702b9dc4386e384c5d9811c401421..07f7167fdb4a9c1e95924535613031ec055697dc 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol4.java @@ -37,8 +37,6 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.Collections; -import java.util.HashMap; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,6 +46,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import jenkins.AgentProtocol; import jenkins.model.identity.InstanceIdentityProvider; +import org.jenkinsci.Symbol; import org.jenkinsci.remoting.engine.JnlpConnectionState; import org.jenkinsci.remoting.engine.JnlpProtocol4Handler; import org.jenkinsci.remoting.protocol.IOHub; @@ -58,10 +57,11 @@ import org.jenkinsci.remoting.protocol.cert.PublicKeyMatchingX509ExtendedTrustMa * *

@see {@link org.jenkinsci.remoting.engine.JnlpProtocol4Handler} for more details. * - * @since 2.27 available as the experimental protocol - * @since TODO enabled by default + * @since 2.27 available as experimental protocol + * @since 2.41 enabled by default */ @Extension +@Symbol("jnlp4") public class JnlpSlaveAgentProtocol4 extends AgentProtocol { /** * Our logger. diff --git a/core/src/main/java/jenkins/slaves/PingFailureAnalyzer.java b/core/src/main/java/jenkins/slaves/PingFailureAnalyzer.java index aec9d8170bc1646c02fa3d5e5c1ad517e72ee80c..5b43ac0d168c2cb4fa94e2fba099d30f31290e36 100644 --- a/core/src/main/java/jenkins/slaves/PingFailureAnalyzer.java +++ b/core/src/main/java/jenkins/slaves/PingFailureAnalyzer.java @@ -28,6 +28,6 @@ public abstract class PingFailureAnalyzer implements ExtensionPoint { public abstract void onPingFailure(Channel c, Throwable cause) throws IOException; public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(PingFailureAnalyzer.class); + return Jenkins.get().getExtensionList(PingFailureAnalyzer.class); } } diff --git a/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java b/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..d1e0c06e4b55a6980f54cb0b52e7785ad89278d9 --- /dev/null +++ b/core/src/main/java/jenkins/slaves/RemotingVersionInfo.java @@ -0,0 +1,113 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.slaves; + +import hudson.util.VersionNumber; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides information about Remoting versions used within the core. + * @author Oleg Nenashev + * @since unrestricted since 2.104, initially added in 2.100. + */ +public class RemotingVersionInfo { + + private static final Logger LOGGER = Logger.getLogger(RemotingVersionInfo.class.getName()); + private static final String RESOURCE_NAME="remoting-info.properties"; + + @Nonnull + private static VersionNumber EMBEDDED_VERSION; + + @Nonnull + private static VersionNumber MINIMUM_SUPPORTED_VERSION; + + private RemotingVersionInfo() {} + + static { + Properties props = new Properties(); + try (InputStream is = RemotingVersionInfo.class.getResourceAsStream(RESOURCE_NAME)) { + if(is!=null) { + props.load(is); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to load Remoting Info from " + RESOURCE_NAME, e); + } + + EMBEDDED_VERSION = extractVersion(props, "remoting.embedded.version"); + MINIMUM_SUPPORTED_VERSION = extractVersion(props, "remoting.minimum.supported.version"); + } + + @Nonnull + private static VersionNumber extractVersion(@Nonnull Properties props, @Nonnull String propertyName) + throws ExceptionInInitializerError { + String prop = props.getProperty(propertyName); + if (prop == null) { + throw new ExceptionInInitializerError(String.format( + "Property %s is not defined in %s", propertyName, RESOURCE_NAME)); + } + + if(prop.contains("${")) { // Due to whatever reason, Maven does not nullify them + throw new ExceptionInInitializerError(String.format( + "Property %s in %s has unresolved variable(s). Raw value: %s", + propertyName, RESOURCE_NAME, prop)); + } + + try { + return new VersionNumber(prop); + } catch (RuntimeException ex) { + throw new ExceptionInInitializerError(new IOException( + String.format("Failed to parse version for for property %s in %s. Raw Value: %s", + propertyName, RESOURCE_NAME, prop), ex)); + } + } + + /** + * Returns a version which is embedded into the Jenkins core. + * Note that this version may differ from one which is being really used in Jenkins. + * @return Remoting version + */ + @Nonnull + public static VersionNumber getEmbeddedVersion() { + return EMBEDDED_VERSION; + } + + /** + * Gets Remoting version which is supported by the core. + * Jenkins core and plugins make invoke operations on agents (e.g. {@link jenkins.security.MasterToSlaveCallable}) + * and use Remoting-internal API within them. + * In such case this API should be present on the remote side. + * This method defines a minimum expected version, so that all calls should use a compatible API. + * @return Minimal Remoting version for API calls. + */ + @Nonnull + public static VersionNumber getMinimumSupportedVersion() { + return MINIMUM_SUPPORTED_VERSION; + } +} diff --git a/core/src/main/java/jenkins/slaves/RemotingWorkDirSettings.java b/core/src/main/java/jenkins/slaves/RemotingWorkDirSettings.java new file mode 100644 index 0000000000000000000000000000000000000000..f49a34564a6c793afc0b74dbb4fa17067c876f0d --- /dev/null +++ b/core/src/main/java/jenkins/slaves/RemotingWorkDirSettings.java @@ -0,0 +1,229 @@ +/* + * The MIT License + * + * Copyright (c) 2017 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.slaves; + +import hudson.Extension; +import hudson.Util; +import hudson.model.Describable; +import hudson.model.Descriptor; +import hudson.model.Slave; +import hudson.slaves.SlaveComputer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Defines settings of the Remoting work directory. + * + * This class contains Remoting Work Directory settings, which can be used when starting Jenkins agents. + * See Remoting Work Dir Documentation. + * + * @author Oleg Nenashev + * @since 2.72 + */ +public class RemotingWorkDirSettings implements Describable { + + private static final String DEFAULT_INTERNAL_DIR = "remoting"; + private static final RemotingWorkDirSettings LEGACY_DEFAULT = new RemotingWorkDirSettings(true, null, DEFAULT_INTERNAL_DIR, false); + private static final RemotingWorkDirSettings ENABLED_DEFAULT = new RemotingWorkDirSettings(false, null, DEFAULT_INTERNAL_DIR, false); + + + private final boolean disabled; + @CheckForNull + private final String workDirPath; + @Nonnull + private final String internalDir; + private final boolean failIfWorkDirIsMissing; + + @DataBoundConstructor + public RemotingWorkDirSettings(boolean disabled, + @CheckForNull String workDirPath, @CheckForNull String internalDir, + boolean failIfWorkDirIsMissing) { + this.disabled = disabled; + this.workDirPath = Util.fixEmptyAndTrim(workDirPath); + this.failIfWorkDirIsMissing = failIfWorkDirIsMissing; + String internalDirName = Util.fixEmptyAndTrim(internalDir); + this.internalDir = internalDirName != null ? internalDirName : DEFAULT_INTERNAL_DIR; + } + + public RemotingWorkDirSettings() { + // Enabled by default + this(false, null, DEFAULT_INTERNAL_DIR, false); + } + + /** + * Check if workdir is disabled. + * + * @return {@code true} if the property is disabled. + * In such case Remoting will use the legacy mode. + */ + public boolean isDisabled() { + return disabled; + } + + /** + * Indicates that agent root directory should be used as work directory. + * + * @return {@code true} if the agent root should be a work directory. + */ + public boolean isUseAgentRootDir() { + return workDirPath == null; + } + + /** + * Check if startup should fail if the workdir is missing. + * + * @return {@code true} if Remoting should fail if the the work directory is missing instead of creating it + */ + public boolean isFailIfWorkDirIsMissing() { + return failIfWorkDirIsMissing; + } + + /** + * Gets path to the custom workdir path. + * + * @return Custom workdir path. + * If {@code null}, an agent root directory path should be used instead. + */ + @CheckForNull + public String getWorkDirPath() { + return workDirPath; + } + + @Nonnull + public String getInternalDir() { + return internalDir; + } + + @Override + public Descriptor getDescriptor() { + return Jenkins.get().getDescriptor(RemotingWorkDirSettings.class); + } + + /** + * Gets list of command-line arguments for the work directory. + * @param computer Computer, for which the arguments are being created + * @return Non-modifiable list of command-line arguments + */ + public List toCommandLineArgs(@Nonnull SlaveComputer computer) { + if(disabled) { + return Collections.emptyList(); + } + + ArrayList args = new ArrayList<>(); + args.add("-workDir"); + if (workDirPath == null) { + Slave node = computer.getNode(); + if (node == null) { + // It is not possible to launch this node anyway. + return Collections.emptyList(); + } + args.add(node.getRemoteFS()); + } else { + args.add(workDirPath); + } + + if (!DEFAULT_INTERNAL_DIR.equals(internalDir)) { + args.add("-internalDir"); + args.add(internalDir); + } + + if (failIfWorkDirIsMissing) { + args.add(" -failIfWorkDirIsMissing"); + } + + return Collections.unmodifiableList(args); + } + + /** + * Gets a command line string, which can be passed to agent start command. + * + * @param computer Computer, for which the arguments need to be constructed. + * @return Command line arguments. + * It may be empty if the working directory is disabled or + * if the Computer type is not {@link SlaveComputer}. + */ + @Nonnull + @Restricted(NoExternalUse.class) + public String toCommandLineString(@Nonnull SlaveComputer computer) { + if(disabled) { + return ""; + } + + StringBuilder bldr = new StringBuilder(); + bldr.append("-workDir \""); + if (workDirPath == null) { + Slave node = computer.getNode(); + if (node == null) { + // It is not possible to launch this node anyway. + return ""; + } + bldr.append(node.getRemoteFS()); + } else { + bldr.append(workDirPath); + } + bldr.append("\""); + + if (!DEFAULT_INTERNAL_DIR.equals(internalDir)) { + bldr.append(" -internalDir \""); + bldr.append(internalDir); + bldr.append("\""); + } + + if (failIfWorkDirIsMissing) { + bldr.append(" -failIfWorkDirIsMissing"); + } + + return bldr.toString(); + } + + @Extension + public static class DescriptorImpl extends Descriptor { + + } + + /** + * Gets default settings for the disabled work directory. + * + * @return Legacy value: disabled work directory. + */ + @Nonnull + public static RemotingWorkDirSettings getDisabledDefaults() { + return LEGACY_DEFAULT; + } + + /** + * Gets default settings of the enabled work directory. + */ + @Nonnull + public static RemotingWorkDirSettings getEnabledDefaults() { + return ENABLED_DEFAULT; + } +} diff --git a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java index 89fedfd04ff52a602b547d70b8bb12acdbaafbc6..9a8d10a12fb6121f002d030358ea9bc73803b5eb 100644 --- a/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java +++ b/core/src/main/java/jenkins/slaves/StandardOutputSwapper.java @@ -39,9 +39,7 @@ public class StandardOutputSwapper extends ComputerListener { private static final class ChannelSwapper extends MasterToSlaveCallable { public Boolean call() throws Exception { if (File.pathSeparatorChar==';') return false; // Windows - - Channel c = Channel.current(); - + Channel c = getOpenChannelOrFail(); StandardOutputStream sos = (StandardOutputStream) c.getProperty(StandardOutputStream.class); if (sos!=null) { swap(sos); diff --git a/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java b/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java index 1eb68dcc6f6da09528e9e35f0e0c40f39be21954..e9495e9ec555dfe27c874595b533706b3ae3c129 100644 --- a/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java +++ b/core/src/main/java/jenkins/slaves/restarter/JnlpSlaveRestarterInstaller.java @@ -16,6 +16,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.Callable; import java.util.logging.Logger; import static java.util.logging.Level.*; @@ -34,67 +35,80 @@ import jenkins.security.MasterToSlaveCallable; public class JnlpSlaveRestarterInstaller extends ComputerListener implements Serializable { @Override public void onOnline(final Computer c, final TaskListener listener) throws IOException, InterruptedException { - MasterComputer.threadPoolForRemoting.submit(new java.util.concurrent.Callable() { - @Override - public Void call() throws Exception { - install(c, listener); - return null; - } - }); + MasterComputer.threadPoolForRemoting.submit(new Install(c, listener)); + } + private static class Install implements Callable { + private final Computer c; + private final TaskListener listener; + Install(Computer c, TaskListener listener) { + this.c = c; + this.listener = listener; + } + @Override + public Void call() throws Exception { + install(c, listener); + return null; + } } - private void install(Computer c, TaskListener listener) { + private static void install(Computer c, TaskListener listener) { try { - final List restarters = new ArrayList(SlaveRestarter.all()); + final List restarters = new ArrayList<>(SlaveRestarter.all()); VirtualChannel ch = c.getChannel(); if (ch==null) return; // defensive check - List effective = ch.call(new MasterToSlaveCallable, IOException>() { - public List call() throws IOException { - Engine e = Engine.current(); - if (e == null) return null; // not running under Engine + List effective = ch.call(new FindEffectiveRestarters(restarters)); - try { - Engine.class.getMethod("addListener", EngineListener.class); - } catch (NoSuchMethodException _) { - return null; // running with older version of remoting that doesn't support adding listener - } + LOGGER.log(FINE, "Effective SlaveRestarter on {0}: {1}", new Object[] {c.getName(), effective}); + } catch (Throwable e) { + Functions.printStackTrace(e, listener.error("Failed to install restarter")); + } + } + private static class FindEffectiveRestarters extends MasterToSlaveCallable, IOException> { + private final List restarters; + FindEffectiveRestarters(List restarters) { + this.restarters = restarters; + } + @Override + public List call() throws IOException { + Engine e = Engine.current(); + if (e == null) return null; // not running under Engine - // filter out ones that doesn't apply - for (Iterator itr = restarters.iterator(); itr.hasNext(); ) { - SlaveRestarter r = itr.next(); - if (!r.canWork()) - itr.remove(); - } + try { + Engine.class.getMethod("addListener", EngineListener.class); + } catch (NoSuchMethodException ignored) { + return null; // running with older version of remoting that doesn't support adding listener + } - e.addListener(new EngineListenerAdapter() { - @Override - public void onReconnect() { + // filter out ones that doesn't apply + for (Iterator itr = restarters.iterator(); itr.hasNext(); ) { + SlaveRestarter r = itr.next(); + if (!r.canWork()) + itr.remove(); + } + + e.addListener(new EngineListenerAdapter() { + @Override + public void onReconnect() { + try { + for (SlaveRestarter r : restarters) { try { - for (SlaveRestarter r : restarters) { - try { - LOGGER.info("Restarting agent via "+r); - r.restart(); - } catch (Exception x) { - LOGGER.log(SEVERE, "Failed to restart agent with "+r, x); - } - } - } finally { - // if we move on to the reconnection without restart, - // don't let the current implementations kick in when the agent loses connection again - restarters.clear(); + LOGGER.info("Restarting agent via "+r); + r.restart(); + } catch (Exception x) { + LOGGER.log(SEVERE, "Failed to restart agent with "+r, x); } } - }); - - return restarters; + } finally { + // if we move on to the reconnection without restart, + // don't let the current implementations kick in when the agent loses connection again + restarters.clear(); + } } }); - LOGGER.log(FINE, "Effective SlaveRestarter on {0}: {1}", new Object[] {c.getName(), effective}); - } catch (Throwable e) { - Functions.printStackTrace(e, listener.error("Failed to install restarter")); + return restarters; } } diff --git a/core/src/main/java/jenkins/slaves/restarter/UnixSlaveRestarter.java b/core/src/main/java/jenkins/slaves/restarter/UnixSlaveRestarter.java index 1572c2301b3e01585dd33cb6ff318c3f45cb035e..086c044efa52ddcd837f32da5412838b987320de 100644 --- a/core/src/main/java/jenkins/slaves/restarter/UnixSlaveRestarter.java +++ b/core/src/main/java/jenkins/slaves/restarter/UnixSlaveRestarter.java @@ -37,13 +37,7 @@ public class UnixSlaveRestarter extends SlaveRestarter { LIBC.execv("positively/no/such/executable", new StringArray(new String[]{"a","b","c"})); return true; - } catch (UnsupportedOperationException e) { - LOGGER.log(FINE, getClass()+" unsuitable", e); - return false; - } catch (LinkageError e) { - LOGGER.log(FINE, getClass()+" unsuitable", e); - return false; - } catch (IOException e) { + } catch (UnsupportedOperationException | LinkageError | IOException e) { LOGGER.log(FINE, getClass()+" unsuitable", e); return false; } diff --git a/core/src/main/java/jenkins/slaves/restarter/WinswSlaveRestarter.java b/core/src/main/java/jenkins/slaves/restarter/WinswSlaveRestarter.java index 8dbdc8ab898588a5f462faf224197076a46671ce..80d291e69dc6ca61ef3387e408dd6052794b07df 100644 --- a/core/src/main/java/jenkins/slaves/restarter/WinswSlaveRestarter.java +++ b/core/src/main/java/jenkins/slaves/restarter/WinswSlaveRestarter.java @@ -24,10 +24,7 @@ public class WinswSlaveRestarter extends SlaveRestarter { return false; // not under winsw return exec("status") ==0; - } catch (InterruptedException e) { - LOGGER.log(FINE, getClass()+" unsuitable", e); - return false; - } catch (IOException e) { + } catch (InterruptedException | IOException e) { LOGGER.log(FINE, getClass()+" unsuitable", e); return false; } diff --git a/core/src/main/java/jenkins/slaves/systemInfo/SlaveSystemInfo.java b/core/src/main/java/jenkins/slaves/systemInfo/SlaveSystemInfo.java index 16a5352d09aad470496420168cc9d4e267a1b79e..8888ca426350c1a2e1b3e9e139dc9267244024df 100644 --- a/core/src/main/java/jenkins/slaves/systemInfo/SlaveSystemInfo.java +++ b/core/src/main/java/jenkins/slaves/systemInfo/SlaveSystemInfo.java @@ -8,7 +8,7 @@ import hudson.model.Computer; * Extension point that contributes to the system information page of {@link Computer}. * *

Views

- * Subtypes must have systemInfo.groovy/.jelly view. + * Subtypes must have {@code systemInfo.groovy/.jelly} view. * This view will have the "it" variable that refers to {@link Computer} object, and "instance" variable * that refers to {@link SlaveSystemInfo} object. * diff --git a/core/src/main/java/jenkins/tasks/SimpleBuildStep.java b/core/src/main/java/jenkins/tasks/SimpleBuildStep.java index 640ea01aa25b35dc754b0b8a6844fbcb128b2770..83a6c5a9ce2f89fab360422bcb3b4adffbc77138 100644 --- a/core/src/main/java/jenkins/tasks/SimpleBuildStep.java +++ b/core/src/main/java/jenkins/tasks/SimpleBuildStep.java @@ -52,7 +52,7 @@ import jenkins.model.DependencyDeclarer; import jenkins.model.RunAction2; import jenkins.model.TransientActionFactory; import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * A build step (like a {@link Builder} or {@link Publisher}) which may be called at an arbitrary time during a build (or multiple times), run, and be done. @@ -103,7 +103,7 @@ public interface SimpleBuildStep extends BuildStep { } @SuppressWarnings("rawtypes") - @Restricted(DoNotUse.class) + @Restricted(NoExternalUse.class) @Extension public static final class LastBuildActionFactory extends TransientActionFactory { diff --git a/core/src/main/java/jenkins/telemetry/Correlator.java b/core/src/main/java/jenkins/telemetry/Correlator.java new file mode 100644 index 0000000000000000000000000000000000000000..9e9005934963c949c3bc5261adf29cbdd828ab98 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/Correlator.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.telemetry; + +import com.google.common.annotations.VisibleForTesting; +import hudson.Extension; +import hudson.model.Describable; +import hudson.model.Descriptor; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.util.UUID; + +/** + * This class stores a UUID identifying this instance for telemetry reporting to allow deduplication or merging of submitted records. + * + * We're not using anything derived from instance identity so we cannot connect an instance's public appearance with its submissions. + * + * This really only uses Descriptor/Describable to get a Saveable implementation for free. + */ +@Extension +@Restricted(NoExternalUse.class) +public class Correlator extends Descriptor implements Describable { + private String correlationId; + + public Correlator() { + super(Correlator.class); + load(); + if (correlationId == null) { + correlationId = UUID.randomUUID().toString(); + save(); + } + } + + public String getCorrelationId() { + return correlationId; + } + + @Restricted(NoExternalUse.class) + @VisibleForTesting + void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } + + @Override + public Descriptor getDescriptor() { + return this; + } +} \ No newline at end of file diff --git a/core/src/main/java/jenkins/telemetry/Telemetry.java b/core/src/main/java/jenkins/telemetry/Telemetry.java new file mode 100644 index 0000000000000000000000000000000000000000..d528f5e9ed53857ad358c6f792d0b90d0a61758a --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/Telemetry.java @@ -0,0 +1,222 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.telemetry; + +import com.google.common.annotations.VisibleForTesting; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.ProxyConfiguration; +import hudson.model.AsyncPeriodicWork; +import hudson.model.TaskListener; +import hudson.model.UsageStatistics; +import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; +import net.sf.json.JSONObject; +import org.apache.commons.codec.digest.DigestUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Extension point for collecting JEP-214 telemetry. + * + * Implementations should provide a description.jelly file with additional details about their purpose and + * behavior which will be included in help-usageStatisticsCollected.jelly for {@link UsageStatistics}. + * + * @see JEP-214 + * + * @since 2.143 + */ +public abstract class Telemetry implements ExtensionPoint { + + // https://webhook.site is a nice stand-in for this during development; just needs to end in ? to submit the ID as query parameter + @Restricted(NoExternalUse.class) + @VisibleForTesting + static String ENDPOINT = SystemProperties.getString(Telemetry.class.getName() + ".endpoint", "https://uplink.jenkins.io/events"); + + private static final Logger LOGGER = Logger.getLogger(Telemetry.class.getName()); + + /** + * ID of this collector, typically an alphanumeric string (and punctuation). + * + * Good IDs are globally unique and human readable (i.e. no UUIDs). + * + * For a periodically updated list of all public implementations, see https://jenkins.io/doc/developer/extensions/jenkins-core/#telemetry + * + * @return ID of the collector, never null or empty + */ + @Nonnull + public String getId() { + return getClass().getName(); + } + + /** + * User friendly display name for this telemetry collector, ideally localized. + * + * @return display name, never null or empty + */ + @Nonnull + public abstract String getDisplayName(); + + /** + * Start date for the collection. + * Will be checked in Jenkins to not collect outside the defined time span. + * This does not have to be precise enough for time zones to be a consideration. + * + * @return collection start date + */ + @Nonnull + public abstract LocalDate getStart(); + + /** + * End date for the collection. + * Will be checked in Jenkins to not collect outside the defined time span. + * This does not have to be precise enough for time zones to be a consideration. + * + * @return collection end date + */ + @Nonnull + public abstract LocalDate getEnd(); + + /** + * Returns the content to be sent to the telemetry service. + * + * This method is called periodically, once per content submission. + * + * @return The JSON payload, or null if no content should be submitted. + */ + @CheckForNull + public abstract JSONObject createContent(); + + public static ExtensionList all() { + return ExtensionList.lookup(Telemetry.class); + } + + /** + * @since 2.147 + * @return whether to collect telemetry + */ + public static boolean isDisabled() { + if (UsageStatistics.DISABLED) { + return true; + } + Jenkins jenkins = Jenkins.getInstanceOrNull(); + + return jenkins == null || !jenkins.isUsageStatisticsCollected(); + } + + @Extension + public static class TelemetryReporter extends AsyncPeriodicWork { + + public TelemetryReporter() { + super("telemetry collection"); + } + + @Override + public long getRecurrencePeriod() { + return TimeUnit.HOURS.toMillis(24); + } + + @Override + protected void execute(TaskListener listener) throws IOException, InterruptedException { + if (isDisabled()) { + LOGGER.info("Collection of anonymous usage statistics is disabled, skipping telemetry collection and submission"); + return; + } + Telemetry.all().forEach(telemetry -> { + if (telemetry.getStart().isAfter(LocalDate.now())) { + LOGGER.config("Skipping telemetry for '" + telemetry.getId() + "' as it is configured to start later"); + return; + } + if (telemetry.getEnd().isBefore(LocalDate.now())) { + LOGGER.config("Skipping telemetry for '" + telemetry.getId() + "' as it is configured to end in the past"); + return; + } + + JSONObject data = new JSONObject(); + try { + data = telemetry.createContent(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to build telemetry content for: '" + telemetry.getId() + "'", e); + } + + if (data == null) { + LOGGER.log(Level.CONFIG, "Skipping telemetry for '" + telemetry.getId() + "' as it has no data"); + return; + } + + JSONObject wrappedData = new JSONObject(); + wrappedData.put("type", telemetry.getId()); + wrappedData.put("payload", data); + String correlationId = ExtensionList.lookupSingleton(Correlator.class).getCorrelationId(); + wrappedData.put("correlator", DigestUtils.sha256Hex(correlationId + telemetry.getId())); + + try { + URL url = new URL(ENDPOINT); + URLConnection conn = ProxyConfiguration.open(url); + if (!(conn instanceof HttpURLConnection)) { + LOGGER.config("URL did not result in an HttpURLConnection: " + ENDPOINT); + return; + } + HttpURLConnection http = (HttpURLConnection) conn; + http.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + http.setDoOutput(true); + + String body = wrappedData.toString(); + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.finest("Submitting JSON: " + body); + } + + try (OutputStream out = http.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + writer.append(body); + } + + LOGGER.config("Telemetry submission received response '" + http.getResponseCode() + " " + http.getResponseMessage() + "' for: " + telemetry.getId()); + } catch (MalformedURLException e) { + LOGGER.config("Malformed endpoint URL: " + ENDPOINT + " for telemetry: " + telemetry.getId()); + } catch (IOException e) { + // deliberately low visibility, as temporary infra problems aren't a big deal and we'd + // rather have some unsuccessful submissions than admins opting out to clean up logs + LOGGER.log(Level.CONFIG, "Failed to submit telemetry: " + telemetry.getId() + " to: " + ENDPOINT, e); + } + }); + } + } +} diff --git a/core/src/main/java/jenkins/telemetry/impl/SecuritySystemProperties.java b/core/src/main/java/jenkins/telemetry/impl/SecuritySystemProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..1384946bed5dd3160ce7b49baefccb26376a0c58 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/SecuritySystemProperties.java @@ -0,0 +1,130 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.telemetry.impl; + +import hudson.Extension; +import jenkins.model.DownloadSettings; +import jenkins.model.Jenkins; +import jenkins.telemetry.Telemetry; +import jenkins.util.SystemProperties; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.Date; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; + +/** + * Telemetry implementation gathering information about system properties. + */ +@Extension +@Restricted(NoExternalUse.class) +public class SecuritySystemProperties extends Telemetry { + @Nonnull + @Override + public String getId() { + return "security-system-properties"; + } + + @Nonnull + @Override + public LocalDate getStart() { + return LocalDate.of(2018, 9, 1); + } + + @Nonnull + @Override + public LocalDate getEnd() { + return LocalDate.of(2018, 12, 1); + } + + @Nonnull + @Override + public String getDisplayName() { + return "Use of Security-related Java system properties"; + } + + @Nonnull + @Override + public JSONObject createContent() { + Map security = new TreeMap<>(); + putBoolean(security, "hudson.ConsoleNote.INSECURE", false); + putBoolean(security, "hudson.logging.LogRecorderManager.skipPermissionCheck", false); + putBoolean(security, "hudson.model.AbstractItem.skipPermissionCheck", false); + putBoolean(security, "hudson.model.ParametersAction.keepUndefinedParameters", false); + putBoolean(security, "hudson.model.Run.skipPermissionCheck", false); + putBoolean(security, "hudson.model.UpdateCenter.skipPermissionCheck", false); + putBoolean(security, "hudson.model.User.allowNonExistentUserToLogin", false); + putBoolean(security, "hudson.model.User.allowUserCreationViaUrl", false); + putBoolean(security, "hudson.model.User.SECURITY_243_FULL_DEFENSE", true); + putBoolean(security, "hudson.model.User.skipPermissionCheck", false); + putBoolean(security, "hudson.PluginManager.skipPermissionCheck", false); + putBoolean(security, "hudson.remoting.URLDeserializationHelper.avoidUrlWrapping", false); + putBoolean(security, "hudson.search.Search.skipPermissionCheck", false); + putBoolean(security, "jenkins.security.ClassFilterImpl.SUPPRESS_WHITELIST", false); + putBoolean(security, "jenkins.security.ClassFilterImpl.SUPPRESS_ALL", false); + putBoolean(security, "org.kohsuke.stapler.Facet.allowViewNamePathTraversal", false); + putBoolean(security, "org.kohsuke.stapler.jelly.CustomJellyContext.escapeByDefault", true); + + // not controlled by a system property for historical reasons only + security.put("jenkins.model.DownloadSettings.useBrowser", Boolean.toString(DownloadSettings.get().isUseBrowser())); + + putStringInfo(security, "hudson.model.ParametersAction.safeParameters"); + putStringInfo(security, "hudson.model.DirectoryBrowserSupport.CSP"); + putStringInfo(security, "hudson.security.HudsonPrivateSecurityRealm.ID_REGEX"); + + Map info = new TreeMap<>(); + info.put("core", Jenkins.getVersion().toString()); + info.put("clientDate", clientDateString()); + info.put("properties", security); + + return JSONObject.fromObject(info); + } + + private static String clientDateString() { + TimeZone tz = TimeZone.getTimeZone("UTC"); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + df.setTimeZone(tz); // strip timezone + return df.format(new Date()); + } + + private static void putBoolean(Map propertiesMap, String systemProperty, boolean defaultValue) { + propertiesMap.put(systemProperty, Boolean.toString(SystemProperties.getBoolean(systemProperty, defaultValue))); + } + + private static void putStringInfo(Map propertiesMap, String systemProperty) { + String reportedValue = "null"; + String value = SystemProperties.getString(systemProperty); + if (value != null) { + reportedValue = Integer.toString(value.length()); + } + propertiesMap.put(systemProperty, reportedValue); + } +} diff --git a/core/src/main/java/jenkins/telemetry/impl/StaplerDispatches.java b/core/src/main/java/jenkins/telemetry/impl/StaplerDispatches.java new file mode 100644 index 0000000000000000000000000000000000000000..6e73a73e9528373fddad273cf48fd76a43aa5d0d --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/StaplerDispatches.java @@ -0,0 +1,116 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.telemetry.impl; + +import hudson.Extension; +import hudson.PluginWrapper; +import hudson.model.UsageStatistics; +import hudson.util.VersionNumber; +import jenkins.model.Jenkins; +import jenkins.telemetry.Telemetry; +import net.sf.json.JSONObject; +import org.kohsuke.MetaInfServices; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.EvaluationTrace; +import org.kohsuke.stapler.StaplerRequest; + +import javax.annotation.Nonnull; +import java.time.LocalDate; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * Telemetry implementation gathering information about Stapler dispatch routes. + */ +@Extension +@Restricted(NoExternalUse.class) +public class StaplerDispatches extends Telemetry { + @Nonnull + @Override + public LocalDate getStart() { + return LocalDate.of(2018, 10, 10); + } + + @Nonnull + @Override + public LocalDate getEnd() { + return LocalDate.of(2019, 2, 1); + } + + @Nonnull + @Override + public String getDisplayName() { + return "Stapler request handling"; + } + + @Override + public JSONObject createContent() { + if (traces.size() == 0) { + return null; + } + Map info = new TreeMap<>(); + info.put("components", buildComponentInformation()); + info.put("dispatches", buildDispatches()); + + return JSONObject.fromObject(info); + } + + private Object buildDispatches() { + Set currentTraces = new TreeSet<>(traces); + traces.clear(); + return currentTraces; + } + + private Object buildComponentInformation() { + Map components = new TreeMap<>(); + VersionNumber core = Jenkins.getVersion(); + components.put("jenkins-core", core == null ? "" : core.toString()); + + for (PluginWrapper plugin : Jenkins.get().pluginManager.getPlugins()) { + if (plugin.isActive()) { + components.put(plugin.getShortName(), plugin.getVersion()); + } + } + return components; + } + + @MetaInfServices + public static class StaplerTrace extends EvaluationTrace.ApplicationTracer { + + @Override + protected void record(StaplerRequest staplerRequest, String s) { + if (Telemetry.isDisabled()) { + // do not collect traces while usage statistics are disabled + return; + } + traces.add(s); + } + } + + private static final Set traces = new ConcurrentSkipListSet<>(); +} diff --git a/core/src/main/java/jenkins/telemetry/impl/UserLanguages.java b/core/src/main/java/jenkins/telemetry/impl/UserLanguages.java new file mode 100644 index 0000000000000000000000000000000000000000..3856cea6344bff7069c0c20dc9f779a209fc2e1f --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/UserLanguages.java @@ -0,0 +1,147 @@ +/* + * The MIT License + * + * Copyright (c) 2018, Daniel Beck + * + * 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 jenkins.telemetry.impl; + +import hudson.Extension; +import hudson.init.Initializer; +import hudson.util.PluginServletFilter; +import jenkins.telemetry.Telemetry; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.time.LocalDate; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Extension +@Restricted(NoExternalUse.class) +public class UserLanguages extends Telemetry { + + private static final Map requestsByLanguage = new ConcurrentSkipListMap<>(); + private static Logger LOGGER = Logger.getLogger(UserLanguages.class.getName()); + + @Nonnull + @Override + public String getId() { + return UserLanguages.class.getName(); + } + + @Nonnull + @Override + public String getDisplayName() { + return "Browser languages"; + } + + @Nonnull + @Override + public LocalDate getStart() { + return LocalDate.of(2018, 10, 1); + } + + @Nonnull + @Override + public LocalDate getEnd() { + return LocalDate.of(2019, 1, 1); + } + + @Override + public JSONObject createContent() { + if (requestsByLanguage.size() == 0) { + return null; + } + Map currentRequests = new TreeMap<>(requestsByLanguage); + requestsByLanguage.clear(); + + JSONObject payload = new JSONObject(); + for (Map.Entry entry : currentRequests.entrySet()) { + payload.put(entry.getKey(), entry.getValue().longValue()); + } + return payload; + } + + @Initializer + public static void setUpFilter() { + Filter filter = new AcceptLanguageFilter(); + if (!PluginServletFilter.hasFilter(filter)) { + try { + PluginServletFilter.addFilter(filter); + } catch (ServletException ex) { + LOGGER.log(Level.WARNING, "Failed to set up languages servlet filter", ex); + } + } + } + + public static final class AcceptLanguageFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (request instanceof HttpServletRequest && !Telemetry.isDisabled()) { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String language = httpServletRequest.getHeader("Accept-Language"); + if (language != null) { + if (!requestsByLanguage.containsKey(language)) { + requestsByLanguage.put(language, new AtomicLong(0)); + } + requestsByLanguage.get(language).incrementAndGet(); + } + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + + } + + @Override + public boolean equals(Object obj) { // support PluginServletFilter#hasFilter + return obj != null && obj.getClass() == AcceptLanguageFilter.class; + } + + // findbugs + @Override + public int hashCode() { + return 42; + } + } +} diff --git a/core/src/main/java/jenkins/tools/ToolConfigurationCategory.java b/core/src/main/java/jenkins/tools/ToolConfigurationCategory.java index e785369556d1ec36ea0eaee5e30e080500c4f8d7..5c6d6ce695c0db70749f8f2fdf78b82d70f210e5 100644 --- a/core/src/main/java/jenkins/tools/ToolConfigurationCategory.java +++ b/core/src/main/java/jenkins/tools/ToolConfigurationCategory.java @@ -2,6 +2,7 @@ package jenkins.tools; import hudson.Extension; import jenkins.model.GlobalConfigurationCategory; +import jenkins.management.Messages; /** * Global configuration of tool locations and installers. @@ -12,10 +13,10 @@ import jenkins.model.GlobalConfigurationCategory; public class ToolConfigurationCategory extends GlobalConfigurationCategory { @Override public String getShortDescription() { - return jenkins.management.Messages.ConfigureTools_Description(); + return Messages.ConfigureTools_Description(); } public String getDisplayName() { - return jenkins.management.Messages.ConfigureTools_DisplayName(); + return Messages.ConfigureTools_DisplayName(); } } diff --git a/core/src/main/java/jenkins/triggers/ReverseBuildTrigger.java b/core/src/main/java/jenkins/triggers/ReverseBuildTrigger.java index 79a98a70660e0f4845d42152e934885bd86a82e4..c601cebc2629486ec81d69faaaf24d1914fc406c 100644 --- a/core/src/main/java/jenkins/triggers/ReverseBuildTrigger.java +++ b/core/src/main/java/jenkins/triggers/ReverseBuildTrigger.java @@ -76,6 +76,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; /** @@ -91,6 +92,7 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc private static final Logger LOGGER = Logger.getLogger(ReverseBuildTrigger.class.getName()); + @CheckForNull private String upstreamProjects; private Result threshold = Result.SUCCESS; @@ -109,8 +111,13 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc this.upstreamProjects = upstreamProjects; } + /** + * Gets the upstream projects. + * + * @return Upstream projects or empty("") if upstream projects is null. + */ public String getUpstreamProjects() { - return upstreamProjects; + return Util.fixNull(upstreamProjects); } public Result getThreshold() { @@ -170,7 +177,7 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc } @Override public void buildDependencyGraph(final AbstractProject downstream, DependencyGraph graph) { - for (AbstractProject upstream : Items.fromNameList(downstream.getParent(), upstreamProjects, AbstractProject.class)) { + for (AbstractProject upstream : Items.fromNameList(downstream.getParent(), getUpstreamProjects(), AbstractProject.class)) { graph.addDependency(new DependencyGraph.Dependency(upstream, downstream) { @Override public boolean shouldTriggerBuild(AbstractBuild upstreamBuild, TaskListener listener, List actions) { return shouldTrigger(upstreamBuild, listener); @@ -234,7 +241,7 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc @Extension public static final class RunListenerImpl extends RunListener { static RunListenerImpl get() { - return ExtensionList.lookup(RunListener.class).get(RunListenerImpl.class); + return ExtensionList.lookupSingleton(RunListenerImpl.class); } private Map> upstream2Trigger; @@ -244,7 +251,7 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc } private Map> calculateCache() { - try (ACLContext _ = ACL.as(ACL.SYSTEM)) { + try (ACLContext acl = ACL.as(ACL.SYSTEM)) { final Map> result = new WeakHashMap<>(); for (Job downstream : Jenkins.getInstance().allItems(Job.class)) { ReverseBuildTrigger trigger = @@ -253,7 +260,7 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc continue; } List upstreams = - Items.fromNameList(downstream.getParent(), trigger.upstreamProjects, Job.class); + Items.fromNameList(downstream.getParent(), trigger.getUpstreamProjects(), Job.class); LOGGER.log(Level.FINE, "from {0} see upstreams {1}", new Object[]{downstream, upstreams}); for (Job upstream : upstreams) { if (upstream instanceof AbstractProject && downstream instanceof AbstractProject) { @@ -305,13 +312,13 @@ public final class ReverseBuildTrigger extends Trigger implements Dependenc public static class ItemListenerImpl extends ItemListener { @Override public void onLocationChanged(Item item, final String oldFullName, final String newFullName) { - try (ACLContext _ = ACL.as(ACL.SYSTEM)) { + try (ACLContext acl = ACL.as(ACL.SYSTEM)) { for (Job p : Jenkins.getInstance().allItems(Job.class)) { ReverseBuildTrigger t = ParameterizedJobMixIn.getTrigger(p, ReverseBuildTrigger.class); if (t != null) { String revised = - Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, t.upstreamProjects, - p.getParent()); + Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, + t.getUpstreamProjects(), p.getParent()); if (!revised.equals(t.upstreamProjects)) { t.upstreamProjects = revised; try { diff --git a/core/src/main/java/jenkins/util/BuildListenerAdapter.java b/core/src/main/java/jenkins/util/BuildListenerAdapter.java index a854ecf3358da61596b3883debde13e5890603a5..341aeece866ee41ec7c8f2a671c836b791cce726 100644 --- a/core/src/main/java/jenkins/util/BuildListenerAdapter.java +++ b/core/src/main/java/jenkins/util/BuildListenerAdapter.java @@ -26,17 +26,13 @@ package jenkins.util; import hudson.console.ConsoleNote; import hudson.model.BuildListener; -import hudson.model.Cause; -import hudson.model.Result; import hudson.model.TaskListener; import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; -import java.util.List; /** * Wraps a {@link TaskListener} as a {@link BuildListener} for compatibility with APIs which historically expected the latter. - * Does not support {@link BuildListener#started} or {@link BuildListener#finished}. * * @since 1.577 */ @@ -48,14 +44,6 @@ public final class BuildListenerAdapter implements BuildListener { this.delegate = delegate; } - @Override public void started(List causes) { - throw new UnsupportedOperationException(); - } - - @Override public void finished(Result result) { - throw new UnsupportedOperationException(); - } - @Override public PrintStream getLogger() { return delegate.getLogger(); } diff --git a/core/src/main/java/jenkins/util/FullDuplexHttpService.java b/core/src/main/java/jenkins/util/FullDuplexHttpService.java index 86c8b7e62143c6629cb52d6442dbbda9e3f5fe39..647c2f765d784e3e65a69c95aef4ecdb2057fa54 100644 --- a/core/src/main/java/jenkins/util/FullDuplexHttpService.java +++ b/core/src/main/java/jenkins/util/FullDuplexHttpService.java @@ -24,6 +24,8 @@ package jenkins.util; import hudson.cli.FullDuplexHttpStream; +import hudson.model.RootAction; +import hudson.security.csrf.CrumbExclusion; import hudson.util.ChunkedInputStream; import hudson.util.ChunkedOutputStream; import java.io.IOException; @@ -44,6 +46,8 @@ import org.kohsuke.stapler.StaplerResponse; /** * Server-side counterpart to {@link FullDuplexHttpStream}. + *

+ * To use, bind this to an endpoint with {@link RootAction} (you will also need a {@link CrumbExclusion}). * @since 2.54 */ public abstract class FullDuplexHttpService { diff --git a/core/src/main/java/jenkins/util/JSONSignatureValidator.java b/core/src/main/java/jenkins/util/JSONSignatureValidator.java index 1d23b709847c6cdbaa6f006ee5f2c56d73910e8c..915a5580344904adda3d77edbdff43a58a8c4297 100644 --- a/core/src/main/java/jenkins/util/JSONSignatureValidator.java +++ b/core/src/main/java/jenkins/util/JSONSignatureValidator.java @@ -2,10 +2,15 @@ package jenkins.util; import com.trilead.ssh2.crypto.Base64; import hudson.util.FormValidation; + +import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.nio.file.InvalidPathException; import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.Charsets; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.io.output.TeeOutputStream; import org.jvnet.hudson.crypto.CertificateUtil; @@ -20,7 +25,9 @@ import java.io.OutputStreamWriter; import java.security.DigestOutputStream; import java.security.GeneralSecurityException; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.Signature; +import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; @@ -78,64 +85,158 @@ public class JSONSignatureValidator { CertificateUtil.validatePath(certs, loadTrustAnchors(cf)); } - // this is for computing a digest to check sanity - MessageDigest sha1 = MessageDigest.getInstance("SHA1"); - DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1); - - // this is for computing a signature - Signature sig = Signature.getInstance("SHA1withRSA"); if (certs.isEmpty()) { return FormValidation.error("No certificate found in %s. Cannot verify the signature", name); - } else { - sig.initVerify(certs.get(0)); - } - SignatureOutputStream sos = new SignatureOutputStream(sig); - - // until JENKINS-11110 fix, UC used to serve invalid digest (and therefore unverifiable signature) - // that only covers the earlier portion of the file. This was caused by the lack of close() call - // in the canonical writing, which apparently leave some bytes somewhere that's not flushed to - // the digest output stream. This affects Jenkins [1.424,1,431]. - // Jenkins 1.432 shipped with the "fix" (1eb0c64abb3794edce29cbb1de50c93fa03a8229) that made it - // compute the correct digest, but it breaks all the existing UC json metadata out there. We then - // quickly discovered ourselves in the catch-22 situation. If we generate UC with the correct signature, - // it'll cut off [1.424,1.431] from the UC. But if we don't, we'll cut off [1.432,*). - // - // In 1.433, we revisited 1eb0c64abb3794edce29cbb1de50c93fa03a8229 so that the original "digest"/"signature" - // pair continues to be generated in a buggy form, while "correct_digest"/"correct_signature" are generated - // correctly. - // - // Jenkins should ignore "digest"/"signature" pair. Accepting it creates a vulnerability that allows - // the attacker to inject a fragment at the end of the json. - o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")).close(); - - // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n - // (which is more likely than someone tampering with update center), we can tell - String computedDigest = new String(Base64.encode(sha1.digest())); - String providedDigest = signature.optString("correct_digest"); - if (providedDigest==null) { - return FormValidation.error("No correct_digest parameter in "+name+". This metadata appears to be old."); } - if (!computedDigest.equalsIgnoreCase(providedDigest)) { - String msg = "Digest mismatch: computed=" + computedDigest + " vs expected=" + providedDigest + " in " + name; - if (LOGGER.isLoggable(Level.SEVERE)) { - LOGGER.severe(msg); - LOGGER.severe(o.toString(2)); + + // check the better digest first + FormValidation resultSha512 = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + Signature sig = Signature.getInstance("SHA512withRSA"); + sig.initVerify(certs.get(0)); + resultSha512 = checkSpecificSignature(o, signature, digest, "correct_digest512", sig, "correct_signature512", "SHA-512"); + switch (resultSha512.kind) { + case ERROR: + return resultSha512; + case WARNING: + LOGGER.log(Level.INFO, "JSON data source '" + name + "' does not provide a SHA-512 content checksum or signature. Looking for SHA-1."); + break; + case OK: + // fall through } - return FormValidation.error(msg); + } catch (NoSuchAlgorithmException nsa) { + LOGGER.log(Level.WARNING, "Failed to verify potential SHA-512 digest/signature, falling back to SHA-1", nsa); } - String providedSignature = signature.getString("correct_signature"); - if (!sig.verify(Base64.decode(providedSignature.toCharArray()))) { - return FormValidation.error("Signature in the update center doesn't match with the certificate in "+name); + // if we get here, SHA-512 passed, wasn't provided, or the JRE is terrible. + + MessageDigest digest = MessageDigest.getInstance("SHA1"); + Signature sig = Signature.getInstance("SHA1withRSA"); + sig.initVerify(certs.get(0)); + FormValidation resultSha1 = checkSpecificSignature(o, signature, digest, "correct_digest", sig, "correct_signature", "SHA-1"); + + switch (resultSha1.kind) { + case ERROR: + return resultSha1; + case WARNING: + if (resultSha512.kind == FormValidation.Kind.WARNING) { + // neither signature provided + return FormValidation.error("No correct_signature or correct_signature512 entry found in '" + name + "'."); + } + case OK: + // fall through } if (warning!=null) return warning; return FormValidation.ok(); } catch (GeneralSecurityException e) { - return FormValidation.error(e,"Signature verification failed in "+name); + return FormValidation.error(e, "Signature verification failed in "+name); } } + + /** + * Computes the specified {@code digest} and {@code signature} for the provided {@code json} object and checks whether they match {@code digestEntry} and {@signatureEntry} in the provided {@code signatureJson} object. + * + * @param json the full update-center.json content + * @param signatureJson signature block from update-center.json + * @param digest digest to compute + * @param digestEntry key of the digest entry in {@code signatureJson} to check + * @param signature signature to compute + * @param signatureEntry key of the signature entry in {@code signatureJson} to check + * @param digestName name of the digest used for log/error messages + * @return {@link FormValidation.Kind#WARNING} if digest or signature are not provided, {@link FormValidation.Kind#OK} if check is successful, {@link FormValidation.Kind#ERROR} otherwise. + * @throws IOException if this somehow fails to write the canonical JSON representation to an in-memory stream. + */ + private FormValidation checkSpecificSignature(JSONObject json, JSONObject signatureJson, MessageDigest digest, String digestEntry, Signature signature, String signatureEntry, String digestName) throws IOException { + // this is for computing a digest to check sanity + DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(), digest); + SignatureOutputStream sos = new SignatureOutputStream(signature); + + String providedDigest = signatureJson.optString(digestEntry, null); + if (providedDigest == null) { + return FormValidation.warning("No '" + digestEntry + "' found"); + } + + String providedSignature = signatureJson.optString(signatureEntry, null); + if (providedSignature == null) { + return FormValidation.warning("No '" + signatureEntry + "' found"); + } + + // until JENKINS-11110 fix, UC used to serve invalid digest (and therefore unverifiable signature) + // that only covers the earlier portion of the file. This was caused by the lack of close() call + // in the canonical writing, which apparently leave some bytes somewhere that's not flushed to + // the digest output stream. This affects Jenkins [1.424,1,431]. + // Jenkins 1.432 shipped with the "fix" (1eb0c64abb3794edce29cbb1de50c93fa03a8229) that made it + // compute the correct digest, but it breaks all the existing UC json metadata out there. We then + // quickly discovered ourselves in the catch-22 situation. If we generate UC with the correct signature, + // it'll cut off [1.424,1.431] from the UC. But if we don't, we'll cut off [1.432,*). + // + // In 1.433, we revisited 1eb0c64abb3794edce29cbb1de50c93fa03a8229 so that the original "digest"/"signature" + // pair continues to be generated in a buggy form, while "correct_digest"/"correct_signature" are generated + // correctly. + // + // Jenkins should ignore "digest"/"signature" pair. Accepting it creates a vulnerability that allows + // the attacker to inject a fragment at the end of the json. + json.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos), Charsets.UTF_8)).close(); + + // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n + // (which is more likely than someone tampering with update center), we can tell + + if (!digestMatches(digest.digest(), providedDigest)) { + String msg = digestName + " digest mismatch: expected=" + providedDigest + " in '" + name + "'"; + if (LOGGER.isLoggable(Level.SEVERE)) { + LOGGER.severe(msg); + LOGGER.severe(json.toString(2)); + } + return FormValidation.error(msg); + } + + if (!verifySignature(signature, providedSignature)) { + return FormValidation.error(digestName + " based signature in the update center doesn't match with the certificate in '"+name + "'"); + } + + return FormValidation.ok(); + } + + /** + * Utility method supporting both possible signature formats: Base64 and Hex + */ + private boolean verifySignature(Signature signature, String providedSignature) { + // We can only make one call to Signature#verify here. + // Since we need to potentially check two values (one decoded from hex, the other decoded from base64), + // try hex first: It's almost certainly going to fail decoding if a base64 string was passed. + // It is extremely unlikely for base64 strings to be a valid hex string. + // This way, if it's base64, the #verify call will be skipped, and we continue with the #verify for decoded base64. + // This approach might look unnecessarily clever, but short of having redundant Signature instances, + // there doesn't seem to be a better approach for this. + try { + if (signature.verify(Hex.decodeHex(providedSignature.toCharArray()))) { + return true; + } + } catch (SignatureException|DecoderException ignore) { + // ignore + } + + try { + if (signature.verify(Base64.decode(providedSignature.toCharArray()))) { + return true; + } + } catch (SignatureException|IOException ignore) { + // ignore + } + return false; + } + + /** + * Utility method supporting both possible digest formats: Base64 and Hex + */ + private boolean digestMatches(byte[] digest, String providedDigest) { + return providedDigest.equalsIgnoreCase(Hex.encodeHexString(digest)) || providedDigest.equalsIgnoreCase(new String(Base64.encode(digest))); + } + + protected Set loadTrustAnchors(CertificateFactory cf) throws IOException { // if we trust default root CAs, we end up trusting anyone who has a valid certificate, // which isn't useful at all diff --git a/core/src/main/java/jenkins/util/MemoryReductionUtil.java b/core/src/main/java/jenkins/util/MemoryReductionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..7d7c84a030e6bda00a7f5b62e3246306c2fd72f0 --- /dev/null +++ b/core/src/main/java/jenkins/util/MemoryReductionUtil.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.util; + +import hudson.Util; +import java.util.HashMap; +import java.util.Map; + +/** + * Utilities to reduce memory footprint + * @author Sam Van Oort + */ +public class MemoryReductionUtil { + /** Returns the capacity we need to allocate for a HashMap so it will hold all elements without needing to resize. */ + public static int preallocatedHashmapCapacity(int elementsToHold) { + if (elementsToHold <= 0) { + return 0; + } else if (elementsToHold < 3) { + return elementsToHold+1; + } else { + return elementsToHold+elementsToHold/3; // Default load factor is 0.75, so we want to fill that much. + } + } + + /** Returns a mutable HashMap presized to hold the given number of elements without needing to resize. */ + public static Map getPresizedMutableMap(int elementCount) { + return new HashMap(preallocatedHashmapCapacity(elementCount)); + } + + /** Empty string array, exactly what it says on the tin. Avoids repeatedly created empty array when calling "toArray." */ + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + /** Returns the input strings, but with all values interned. */ + public static String[] internInPlace(String[] input) { + if (input == null) { + return null; + } else if (input.length == 0) { + return EMPTY_STRING_ARRAY; + } + for (int i=0; imilliseconds. + * @deprecated use {@link #getTimeInMillis()} instead. + * + * This method has always returned a time in milliseconds, when various callers incorrectly assumed seconds. + * And this spread through the codebase. So this has been deprecated for clarity in favour of more explicitly named + * methods. + */ + @Deprecated public int getTime() { return (int)millis; } + /** + * Returns the duration of this instance in milliseconds. + */ public long getTimeInMillis() { return millis; } + /** + * Returns the duration of this instance in seconds. + * @since 2.82 + */ + public int getTimeInSeconds() { + return (int) (millis / 1000L); + } + + public long as(TimeUnit t) { return t.convert(millis,TimeUnit.MILLISECONDS); } - public static @CheckForNull TimeDuration fromString(@CheckForNull String delay) { - if (delay==null) + /** + * Creates a {@link TimeDuration} from the delay passed in parameter + * @param delay the delay either in milliseconds without unit, or in seconds if suffixed by sec or secs. + * @return the TimeDuration created from the delay expressed as a String. + */ + @CheckForNull + public static TimeDuration fromString(@CheckForNull String delay) { + if (delay == null) { return null; + } + long unitMultiplier = 1L; + delay = delay.trim(); try { // TODO: more unit handling - if(delay.endsWith("sec")) delay=delay.substring(0,delay.length()-3); - if(delay.endsWith("secs")) delay=delay.substring(0,delay.length()-4); - return new TimeDuration(Long.parseLong(delay)); + if (delay.endsWith("sec") || delay.endsWith("secs")) { + delay = delay.substring(0, delay.lastIndexOf("sec")); + delay = delay.trim(); + unitMultiplier = 1000L; + } + return new TimeDuration(Long.parseLong(delay.trim()) * unitMultiplier); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid time duration value: "+delay); } diff --git a/core/src/main/java/jenkins/util/Timer.java b/core/src/main/java/jenkins/util/Timer.java index b452efa0622cafe7377043d71309a226ef7d5603..65acffe4f7b041d4c94abf405c489b1569fe3259 100644 --- a/core/src/main/java/jenkins/util/Timer.java +++ b/core/src/main/java/jenkins/util/Timer.java @@ -1,6 +1,7 @@ package jenkins.util; import hudson.security.ACL; +import hudson.util.ClassLoaderSanityThreadFactory; import hudson.util.DaemonThreadFactory; import hudson.util.NamingThreadFactory; import javax.annotation.Nonnull; @@ -29,7 +30,8 @@ public class Timer { * The scheduled executor thread pool. This is initialized lazily since it may be created/shutdown many times * when running the test suite. */ - private static ScheduledExecutorService executorService; + static ScheduledExecutorService executorService; + /** * Returns the scheduled executor service used by all timed tasks in Jenkins. @@ -42,7 +44,7 @@ public class Timer { // corePoolSize is set to 10, but will only be created if needed. // ScheduledThreadPoolExecutor "acts as a fixed-sized pool using corePoolSize threads" // TODO consider also wrapping in ContextResettingExecutorService - executorService = new ImpersonatingScheduledExecutorService(new ErrorLoggingScheduledThreadPoolExecutor(10, new NamingThreadFactory(new DaemonThreadFactory(), "jenkins.util.Timer")), ACL.SYSTEM); + executorService = new ImpersonatingScheduledExecutorService(new ErrorLoggingScheduledThreadPoolExecutor(10, new NamingThreadFactory(new ClassLoaderSanityThreadFactory(new DaemonThreadFactory()), "jenkins.util.Timer")), ACL.SYSTEM); } return executorService; } diff --git a/core/src/main/java/jenkins/util/UrlHelper.java b/core/src/main/java/jenkins/util/UrlHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..f5fc20de3f39e71c2251f368aad227874037a55c --- /dev/null +++ b/core/src/main/java/jenkins/util/UrlHelper.java @@ -0,0 +1,99 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.util; + +import jenkins.org.apache.commons.validator.routines.DomainValidator; +import jenkins.org.apache.commons.validator.routines.UrlValidator; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Objective is to validate an URL in a lenient way sufficiently strict to avoid too weird URL + * but to still allow particular internal URL to be accepted + */ +@Restricted(NoExternalUse.class) +public class UrlHelper { + /** + * Authorize the {@code _} and {@code -} characters in domain + *

+ * Avoid {@code -} to be first or last, and {@code .} to be first (but can be last) + *

+ * + * Lenient version of:

    + *
  1. RFC-952 GRAMMATICAL HOST TABLE SPECIFICATION
  2. + *
  3. RFC-1034 3.5
  4. + *
  5. RFC-17383.1, host
  6. + *
  7. RFC-1123 2.1
  8. + *
+ *

+ * + * Deliberately allow:

    + *
  1. short domain name (often there are rules like minimum of 3 characters)
  2. + *
  3. long domain name (normally limit on whole domain of 255 and for each subdomain/label of 63)
  4. + *
  5. starting by numbers (disallowed by RFC-952 and RFC-1034, but nowadays it's supported by RFC-1123)
  6. + *
  7. use of underscore (not explicitly allowed in RFC but could occur in internal network, we do not speak about path here, just domain)
  8. + *
  9. custom TLD like "intern" that is not standard but could be registered locally in a network
  10. + *
+ */ + private static String DOMAIN_REGEX = System.getProperty( + UrlHelper.class.getName() + ".DOMAIN_REGEX", + "^" + + "\\w" + // must start with letter / number / underscore + "(-*(\\.|\\w))*" +// dashes are allowed but not as last character + "\\.*" + // can end with zero (most common), one or multiple dots + "(:\\d{1,5})?" + // and potentially the port specification + "$" + ); + + public static boolean isValidRootUrl(String url) { + UrlValidator validator = new CustomUrlValidator(); + return validator.isValid(url); + } + + private static class CustomUrlValidator extends UrlValidator { + private CustomUrlValidator() { + super(new String[]{"http", "https"}, UrlValidator.ALLOW_LOCAL_URLS + UrlValidator.NO_FRAGMENTS); + } + + @Override + protected boolean isValidAuthority(String authority) { + boolean superResult = super.isValidAuthority(authority); + if(superResult && authority.contains("[")){ + // to support ipv6 + return true; + } + if(!superResult && authority == null){ + return false; + } + String authorityASCII = DomainValidator.unicodeToASCII(authority); + return authorityASCII.matches(DOMAIN_REGEX); + } + + @Override + protected boolean isValidQuery(String query) { + // does not accept query + return query == null; + } + } +} diff --git a/core/src/main/java/jenkins/util/VirtualFile.java b/core/src/main/java/jenkins/util/VirtualFile.java index e2d27d75ec6a050555d6e7cc8f677ac92d803b50..aa8f51dd8393bad629c0937a0daeff938f81c11e 100644 --- a/core/src/main/java/jenkins/util/VirtualFile.java +++ b/core/src/main/java/jenkins/util/VirtualFile.java @@ -25,29 +25,49 @@ package jenkins.util; import hudson.FilePath; +import hudson.Util; import hudson.model.DirectoryBrowserSupport; +import hudson.os.PosixException; import hudson.remoting.Callable; import hudson.remoting.Channel; +import hudson.remoting.RemoteInputStream; import hudson.remoting.VirtualChannel; import hudson.util.DirScanner; import hudson.util.FileVisitor; +import hudson.util.IOUtils; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URI; +import java.net.URL; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import jenkins.MasterToSlaveFileCallable; +import jenkins.model.ArtifactManager; +import jenkins.security.MasterToSlaveCallable; +import org.apache.tools.ant.DirectoryScanner; +import org.apache.tools.ant.types.AbstractFileSet; +import org.apache.tools.ant.types.selectors.SelectorUtils; +import org.apache.tools.ant.types.selectors.TokenizedPath; +import org.apache.tools.ant.types.selectors.TokenizedPattern; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Abstraction over {@link File}, {@link FilePath}, or other items such as network resources or ZIP entries. @@ -62,6 +82,25 @@ import jenkins.MasterToSlaveFileCallable; * {@link VirtualFile} makes no assumption about where the actual files are, or whether there really exists * {@link File}s somewhere. This makes VirtualFile more abstract. * + *

Opening files from other machines

+ * + * While {@link VirtualFile} is marked {@link Serializable}, + * it is not safe in general to transfer over a Remoting channel. + * (For example, an implementation from {@link #forFilePath} could be sent on the same channel, + * but an implementation from {@link #forFile} will not.) + * Thus callers should assume that methods such as {@link #open} will work + * only on the node on which the object was created. + * + *

Since some implementations may in fact use external file storage, + * callers may request optional APIs to access those services more efficiently. + * Otherwise, for example, a plugin copying a file + * previously saved by {@link ArtifactManager} to an external storage service + * which tunneled a stream from {@link #open} using {@link RemoteInputStream} + * would wind up transferring the file from the service to the Jenkins master and then on to an agent. + * Similarly, if {@link DirectoryBrowserSupport} rendered a link to an in-Jenkins URL, + * a large file could be transferred from the service to the Jenkins master and then on to the browser. + * To avoid this overhead, callers may check whether an implementation supports {@link #toExternalURL}. + * * @see DirectoryBrowserSupport * @see FilePath * @since 1.532 @@ -80,9 +119,11 @@ public abstract class VirtualFile implements Comparable, Serializab /** * Gets a URI. * Should at least uniquely identify this virtual file within its root, but not necessarily globally. + *

When {@link #toExternalURL} is implemented, that same value could be used here, + * unless some sort of authentication is also embedded. * @return a URI (need not be absolute) */ - public abstract URI toURI(); + public abstract @Nonnull URI toURI(); /** * Gets the parent file. @@ -105,8 +146,22 @@ public abstract class VirtualFile implements Comparable, Serializab */ public abstract boolean isFile() throws IOException; + /** + * If this file is a symlink, returns the link target. + *

The default implementation always returns null. + * Some implementations may not support symlinks under any conditions. + * @return a target (typically a relative path in some format), or null if this is not a link + * @throws IOException if reading the link, or even determining whether this file is a link, failed + * @since 2.118 + */ + @Restricted(Beta.class) + public @CheckForNull String readLink() throws IOException { + return null; + } + /** * Checks whether this file exists. + * The behavior is undefined for symlinks; if in doubt, check {@link #readLink} first. * @return true if it is a plain file or directory, false if nonexistent * @throws IOException in case checking status failed */ @@ -119,13 +174,75 @@ public abstract class VirtualFile implements Comparable, Serializab */ public abstract @Nonnull VirtualFile[] list() throws IOException; + /** + * @deprecated use {@link #list(String, String, boolean)} instead + */ + @Deprecated + public @Nonnull String[] list(String glob) throws IOException { + return list(glob.replace('\\', '/'), null, true).toArray(MemoryReductionUtil.EMPTY_STRING_ARRAY); + } + /** * Lists recursive files of this directory with pattern matching. - * @param glob an Ant-style glob - * @return a list of relative names of children (files directly inside or in subdirectories) + *

The default implementation calls {@link #list()} recursively inside {@link #run} and applies filtering to the result. + * Implementations may wish to override this more efficiently. + * @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 no matches (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} + * @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.118 */ - public abstract @Nonnull String[] list(String glob) throws IOException; + @Restricted(Beta.class) + public @Nonnull Collection list(@Nonnull String includes, @CheckForNull String excludes, boolean useDefaultExcludes) throws IOException { + Collection r = run(new CollectFiles(this)); + List includePatterns = patterns(includes); + List excludePatterns = patterns(excludes); + if (useDefaultExcludes) { + for (String patt : DirectoryScanner.getDefaultExcludes()) { + excludePatterns.add(new TokenizedPattern(patt.replace('/', File.separatorChar))); + } + } + return r.stream().filter(p -> { + TokenizedPath path = new TokenizedPath(p.replace('/', File.separatorChar)); + return includePatterns.stream().anyMatch(patt -> patt.matchPath(path, true)) && !excludePatterns.stream().anyMatch(patt -> patt.matchPath(path, true)); + }).collect(Collectors.toSet()); + } + private static final class CollectFiles extends MasterToSlaveCallable, IOException> { + private static final long serialVersionUID = 1; + private final VirtualFile root; + CollectFiles(VirtualFile root) { + this.root = root; + } + @Override + public Collection call() throws IOException { + List r = new ArrayList<>(); + collectFiles(root, r, ""); + return r; + } + private static void collectFiles(VirtualFile d, Collection names, String prefix) throws IOException { + for (VirtualFile child : d.list()) { + if (child.isFile()) { + names.add(prefix + child.getName()); + } else if (child.isDirectory()) { + collectFiles(child, names, prefix + child.getName() + "/"); + } + } + } + } + private List patterns(String patts) { + List r = new ArrayList<>(); + if (patts != null) { + for (String patt : patts.split(",")) { + if (patt.endsWith("/")) { + patt += SelectorUtils.DEEP_TREE_MATCH; + } + r.add(new TokenizedPattern(patt.replace('/', File.separatorChar))); + } + } + return r; + } /** * Obtains a child file. @@ -148,6 +265,18 @@ public abstract class VirtualFile implements Comparable, Serializab */ public abstract long lastModified() throws IOException; + /** + * Gets the file’s Unix mode, if meaningful. + * If the file is symlink (see {@link #readLink}), the mode is that of the link target, not the link itself. + * @return for example, 0644 ~ {@code rw-r--r--}; -1 by default, meaning unknown or inapplicable + * @throws IOException if checking the mode failed + * @since 2.118 + */ + @Restricted(Beta.class) + public int mode() throws IOException { + return -1; + } + /** * Checks whether this file can be read. * @return true normally @@ -208,6 +337,49 @@ public abstract class VirtualFile implements Comparable, Serializab return callable.call(); } + /** + * Optionally obtains a URL which may be used to retrieve file contents from any process on any node. + * For example, given cloud storage this might produce a permalink to the file. + *

Only {@code http} and {@code https} protocols are permitted. + * It is recommended to use {@code RobustHTTPClient.downloadFile} to work with these URLs. + *

This is only meaningful for {@link #isFile}: + * no ZIP etc. archiving protocol is defined to allow bulk access to directory trees. + *

Any necessary authentication must be encoded somehow into the URL itself; + * do not include any tokens or other authentication which might allow access to unrelated files + * (for example {@link ArtifactManager} builds from a different job). + * Authentication should be limited to download, not upload or any other modifications. + *

The URL might be valid for only a limited amount of time or even only a single use; + * this method should be called anew every time an external URL is required. + * @return an externally usable URL like {@code https://gist.githubusercontent.com/ACCT/GISTID/raw/COMMITHASH/FILE}, or null if there is no such support + * @since 2.118 + * @see #toURI + */ + @Restricted(Beta.class) + public @CheckForNull URL toExternalURL() throws IOException { + return null; + } + + /** + * Determine if the implementation supports the {@link #isDescendant(String)} method + * + * TODO un-restrict it in a weekly after the patch + */ + @Restricted(NoExternalUse.class) + public boolean supportIsDescendant() { + return false; + } + + /** + * Check if the relative path is really a descendant of this folder, following the symbolic links. + * Meant to be used in coordination with {@link #child(String)}. + * + * TODO un-restrict it in a weekly after the patch + */ + @Restricted(NoExternalUse.class) + public boolean isDescendant(String childRelativePath) throws IOException { + return false; + } + /** * Creates a virtual file wrapper for a local file. * @param f a disk file (need not exist) @@ -250,6 +422,12 @@ public abstract class VirtualFile implements Comparable, Serializab } return f.exists(); } + @Override public String readLink() throws IOException { + if (isIllegalSymlink()) { + return null; // best to just ignore link -> ../whatever + } + return Util.resolveSymlink(f); + } @Override public VirtualFile[] list() throws IOException { if (isIllegalSymlink()) { return new VirtualFile[0]; @@ -264,11 +442,12 @@ public abstract class VirtualFile implements Comparable, Serializab } return vfs; } - @Override public String[] list(String glob) throws IOException { + @Override + public Collection list(String includes, String excludes, boolean useDefaultExcludes) throws IOException { if (isIllegalSymlink()) { - return new String[0]; + return Collections.emptySet(); } - return new Scanner(glob).invoke(f, null); + return new Scanner(includes, excludes, useDefaultExcludes).invoke(f, null); } @Override public VirtualFile child(String name) { return new FileVF(new File(f, name), root); @@ -279,6 +458,12 @@ public abstract class VirtualFile implements Comparable, Serializab } return f.length(); } + @Override public int mode() throws IOException { + if (isIllegalSymlink()) { + return -1; + } + return IOUtils.mode(f); + } @Override public long lastModified() throws IOException { if (isIllegalSymlink()) { return 0; @@ -301,6 +486,7 @@ public abstract class VirtualFile implements Comparable, Serializab throw new IOException(e); } } + private boolean isIllegalSymlink() { // TODO JENKINS-26838 try { String myPath = f.toPath().toRealPath(new LinkOption[0]).toString(); @@ -316,6 +502,54 @@ public abstract class VirtualFile implements Comparable, Serializab } return false; } + + /** + * TODO un-restrict it in a weekly after the patch + */ + @Override + @Restricted(NoExternalUse.class) + public boolean supportIsDescendant() { + return true; + } + + /** + * TODO un-restrict it in a weekly after the patch + */ + @Override + @Restricted(NoExternalUse.class) + public boolean isDescendant(String potentialChildRelativePath) throws IOException { + if (new File(potentialChildRelativePath).isAbsolute()) { + throw new IllegalArgumentException("Only a relative path is supported, the given path is absolute: " + potentialChildRelativePath); + } + + FilePath root = new FilePath(this.root); + String relativePath = computeRelativePathToRoot(); + + try { + return root.isDescendant(relativePath + potentialChildRelativePath); + } + catch (InterruptedException e) { + return false; + } + } + + /** + * To be kept in sync with {@link FilePathVF#computeRelativePathToRoot()} + */ + private String computeRelativePathToRoot(){ + if (this.root.equals(this.f)) { + return ""; + } + + Deque relativePath = new LinkedList<>(); + File current = this.f; + while (current != null && !current.equals(this.root)) { + relativePath.addFirst(current.getName()); + current = current.getParentFile(); + } + + return String.join(File.separator, relativePath) + File.separator; + } } /** @@ -324,12 +558,14 @@ public abstract class VirtualFile implements Comparable, Serializab * @return a wrapper */ public static VirtualFile forFilePath(final FilePath f) { - return new FilePathVF(f); + return new FilePathVF(f, f); } private static final class FilePathVF extends VirtualFile { private final FilePath f; - FilePathVF(FilePath f) { + private final FilePath root; + FilePathVF(FilePath f, FilePath root) { this.f = f; + this.root = root; } @Override public String getName() { return f.getName(); @@ -348,7 +584,7 @@ public abstract class VirtualFile implements Comparable, Serializab try { return f.isDirectory(); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } @Override public boolean isFile() throws IOException { @@ -359,7 +595,14 @@ public abstract class VirtualFile implements Comparable, Serializab try { return f.exists(); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); + } + } + @Override public String readLink() throws IOException { + try { + return f.readLink(); + } catch (InterruptedException x) { + throw new IOException(x); } } @Override public VirtualFile[] list() throws IOException { @@ -367,73 +610,133 @@ public abstract class VirtualFile implements Comparable, Serializab List kids = f.list(); VirtualFile[] vfs = new VirtualFile[kids.size()]; for (int i = 0; i < vfs.length; i++) { - vfs[i] = forFilePath(kids.get(i)); + vfs[i] = new FilePathVF(kids.get(i), this.root); } return vfs; } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } - @Override public String[] list(String glob) throws IOException { + @Override public Collection list(String includes, String excludes, boolean useDefaultExcludes) throws IOException { try { - return f.act(new Scanner(glob)); + return f.act(new Scanner(includes, excludes, useDefaultExcludes)); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } @Override public VirtualFile child(String name) { - return forFilePath(f.child(name)); + return new FilePathVF(f.child(name), this.root); } @Override public long length() throws IOException { try { return f.length(); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); + } + } + @Override public int mode() throws IOException { + try { + return f.mode(); + } catch (InterruptedException | PosixException x) { + throw new IOException(x); } } @Override public long lastModified() throws IOException { try { return f.lastModified(); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } @Override public boolean canRead() throws IOException { try { return f.act(new Readable()); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } @Override public InputStream open() throws IOException { try { return f.read(); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } @Override public V run(Callable callable) throws IOException { try { return f.act(callable); } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); + throw new IOException(x); } } + + /** + * TODO un-restrict it in a weekly after the patch + */ + @Override + @Restricted(NoExternalUse.class) + public boolean supportIsDescendant() { + return true; + } + + /** + * TODO un-restrict it in a weekly after the patch + */ + @Override + @Restricted(NoExternalUse.class) + public boolean isDescendant(String potentialChildRelativePath) throws IOException { + if (new File(potentialChildRelativePath).isAbsolute()) { + throw new IllegalArgumentException("Only a relative path is supported, the given path is absolute: " + potentialChildRelativePath); + } + + String relativePath = computeRelativePathToRoot(); + + try { + return this.root.isDescendant(relativePath + potentialChildRelativePath); + } + catch (InterruptedException e) { + return false; + } + } + + /** + * To be kept in sync with {@link FileVF#computeRelativePathToRoot()} + */ + private String computeRelativePathToRoot(){ + if (this.root.equals(this.f)) { + return ""; + } + + LinkedList relativePath = new LinkedList<>(); + FilePath current = this.f; + while (current != null && !current.equals(this.root)) { + relativePath.addFirst(current.getName()); + current = current.getParent(); + } + + return String.join(File.separator, relativePath) + File.separator; + } } - private static final class Scanner extends MasterToSlaveFileCallable { - private final String glob; - Scanner(String glob) { - this.glob = glob; + private static final class Scanner extends MasterToSlaveFileCallable> { + private final String includes, excludes; + private final boolean useDefaultExcludes; + Scanner(String includes, String excludes, boolean useDefaultExcludes) { + this.includes = includes; + this.excludes = excludes; + this.useDefaultExcludes = useDefaultExcludes; } - @Override public String[] invoke(File f, VirtualChannel channel) throws IOException { + @Override public List invoke(File f, VirtualChannel channel) throws IOException { + if (includes.isEmpty()) { // see Glob class Javadoc, and list(String, String, boolean) note + return Collections.emptyList(); + } final List paths = new ArrayList(); - new DirScanner.Glob(glob, null).scan(f, new FileVisitor() { + new DirScanner.Glob(includes, excludes, useDefaultExcludes).scan(f, new FileVisitor() { @Override public void visit(File f, String relativePath) throws IOException { - paths.add(relativePath); + paths.add(relativePath.replace('\\', '/')); } }); - return paths.toArray(new String[paths.size()]); + return paths; } } diff --git a/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java b/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java new file mode 100644 index 0000000000000000000000000000000000000000..9bc12c6313d07753ab13789a356630ed255d05c3 --- /dev/null +++ b/core/src/main/java/jenkins/util/groovy/AbstractGroovyViewModule.java @@ -0,0 +1,48 @@ +package jenkins.util.groovy; + +import groovy.lang.GroovyObjectSupport; +import lib.FormTagLib; +import lib.LayoutTagLib; +import org.kohsuke.stapler.jelly.groovy.JellyBuilder; +import org.kohsuke.stapler.jelly.groovy.Namespace; +import lib.JenkinsTagLib; + +/** + * Base class for utility classes for Groovy view scripts + *

+ * Usage from script of a subclass, say ViewHelper: + *

+ * {@code new ViewHelper(delegate).method();} + *

+ * see {@code ModularizeViewScript} in ui-samples for an example how to use + * this class. + */ +public abstract class AbstractGroovyViewModule extends GroovyObjectSupport { + + public JellyBuilder builder; + public FormTagLib f; + public LayoutTagLib l; + public JenkinsTagLib t; + public Namespace st; + + public AbstractGroovyViewModule(JellyBuilder b) { + builder = b; + f = builder.namespace(FormTagLib.class); + l = builder.namespace(LayoutTagLib.class); + t = builder.namespace(JenkinsTagLib.class); + st = builder.namespace("jelly:stapler"); + } + + public Object methodMissing(String name, Object args) { + return builder.invokeMethod(name, args); + } + + public Object propertyMissing(String name) { + return builder.getProperty(name); + } + + public void propertyMissing(String name, Object value) { + builder.setProperty(name, value); + } + +} diff --git a/core/src/main/java/jenkins/util/groovy/GroovyHookScript.java b/core/src/main/java/jenkins/util/groovy/GroovyHookScript.java index 3c01ebf827da233145b29d1e80e2a03c706cd573..4f15ed49658e1d3549109dc61c4df529d5a842c0 100644 --- a/core/src/main/java/jenkins/util/groovy/GroovyHookScript.java +++ b/core/src/main/java/jenkins/util/groovy/GroovyHookScript.java @@ -31,8 +31,8 @@ import jenkins.model.Jenkins; * * *

- * Scripts inside /WEB-INF is meant for OEM distributions of Jenkins. Files inside - * $JENKINS_HOME are for installation local settings. Use of HOOK.groovy.d + * Scripts inside {@code /WEB-INF} is meant for OEM distributions of Jenkins. Files inside + * {@code $JENKINS_HOME} are for installation local settings. Use of {@code HOOK.groovy.d} * allows configuration management tools to control scripts easily. * * @author Kohsuke Kawaguchi diff --git a/core/src/main/java/jenkins/util/io/CompositeIOException.java b/core/src/main/java/jenkins/util/io/CompositeIOException.java new file mode 100644 index 0000000000000000000000000000000000000000..dfd347cbd6eb6aa3174ae9cf2be91e36fae13dc5 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/CompositeIOException.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.util.io; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; + +@Restricted(NoExternalUse.class) +public class CompositeIOException extends IOException { + private final List exceptions; + + public CompositeIOException(String message, @Nonnull List exceptions) { + super(message); + this.exceptions = exceptions; + } + + public CompositeIOException(String message, IOException... exceptions) { + this(message, Arrays.asList(exceptions)); + } + + public List getExceptions() { + return exceptions; + } + + @Override + public void printStackTrace(PrintStream s) { + super.printStackTrace(s); + for (IOException exception : exceptions) { + exception.printStackTrace(s); + } + } + + @Override + public void printStackTrace(PrintWriter s) { + super.printStackTrace(s); + for (IOException exception : exceptions) { + exception.printStackTrace(s); + } + } + + public UncheckedIOException asUncheckedIOException() { + return new UncheckedIOException(this); + } +} diff --git a/core/src/main/java/jenkins/util/io/LinesStream.java b/core/src/main/java/jenkins/util/io/LinesStream.java new file mode 100644 index 0000000000000000000000000000000000000000..92931c4ccd1e025df1e08d4c8b591d82453f2205 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/LinesStream.java @@ -0,0 +1,114 @@ +/* + * The MIT License + * + * Copyright 2018 Daniel Trebbien. + * + * 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 jenkins.util.io; + +import com.google.common.collect.AbstractIterator; + +import edu.umd.cs.findbugs.annotations.CleanupObligation; +import edu.umd.cs.findbugs.annotations.DischargesObligation; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a stream over the lines of a text file. + *

+ * Although LinesStream implements {@link java.lang.Iterable}, it + * is intended to be first used to initialize a resource in a try-with-resources + * statement and then iterated, as in: + *

+ *  try (LinesStream stream = new LinesStream(...)) {
+ *      for (String line : stream) {
+ *          ...
+ *      }
+ *  }
+ * 
+ * This pattern ensures that the underlying file handle is closed properly. + *

+ * Like {@link java.nio.file.DirectoryStream}, LinesStream supports + * creating at most one Iterator. Invoking {@link #iterator()} to + * obtain a second or subsequent Iterator throws + * IllegalStateException. + * + * @since 2.111 + */ +@CleanupObligation +public class LinesStream implements Closeable, Iterable { + + private final @Nonnull BufferedReader in; + private transient @Nullable Iterator iterator; + + /** + * Opens the text file at path for reading using charset + * {@link java.nio.charset.StandardCharsets#UTF_8}. + * @param path Path to the file to open for reading. + * @throws IOException if the file at path cannot be opened for + * reading. + */ + public LinesStream(@Nonnull Path path) throws IOException { + in = Files.newBufferedReader(path); // uses UTF-8 by default + } + + @DischargesObligation + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public Iterator iterator() { + if (iterator!=null) + throw new IllegalStateException("Only one Iterator can be created."); + + iterator = new AbstractIterator() { + @Override + protected String computeNext() { + try { + String r = in.readLine(); + if (r==null) { + // Calling close() here helps ensure that the file + // handle is closed even when LinesStream is being used + // incorrectly, where it is iterated over without being + // used to initialize a resource of a try-with-resources + // statement. + in.close(); + return endOfData(); + } + return r; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + + return iterator; + } +} diff --git a/core/src/main/java/jenkins/util/io/PathRemover.java b/core/src/main/java/jenkins/util/io/PathRemover.java new file mode 100644 index 0000000000000000000000000000000000000000..ece2e04bbf5999bed13ecbceafd26cd104a26a94 --- /dev/null +++ b/core/src/main/java/jenkins/util/io/PathRemover.java @@ -0,0 +1,298 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.util.io; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Functions; +import hudson.Util; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Restricted(NoExternalUse.class) +public class PathRemover { + + public static PathRemover newSimpleRemover() { + return new PathRemover(ignored -> false, PathChecker.ALLOW_ALL); + } + + public static PathRemover newRemoverWithStrategy(@Nonnull RetryStrategy retryStrategy) { + return new PathRemover(retryStrategy, PathChecker.ALLOW_ALL); + } + + public static PathRemover newFilteredRobustRemover(@Nonnull PathChecker pathChecker, int maxRetries, boolean gcAfterFailedRemove, long waitBetweenRetries) { + return new PathRemover(new PausingGCRetryStrategy(maxRetries < 1 ? 1 : maxRetries, gcAfterFailedRemove, waitBetweenRetries), pathChecker); + } + + private final RetryStrategy retryStrategy; + private final PathChecker pathChecker; + + private PathRemover(@Nonnull RetryStrategy retryStrategy, @Nonnull PathChecker pathChecker) { + this.retryStrategy = retryStrategy; + this.pathChecker = pathChecker; + } + + public void forceRemoveFile(@Nonnull Path path) throws IOException { + for (int retryAttempts = 0; ; retryAttempts++) { + Optional maybeError = tryRemoveFile(path); + if (!maybeError.isPresent()) return; + if (retryStrategy.shouldRetry(retryAttempts)) continue; + IOException error = maybeError.get(); + throw new IOException(retryStrategy.failureMessage(path, retryAttempts), error); + } + } + + public void forceRemoveDirectoryContents(@Nonnull Path path) throws IOException { + for (int retryAttempt = 0; ; retryAttempt++) { + List errors = tryRemoveDirectoryContents(path); + if (errors.isEmpty()) return; + if (retryStrategy.shouldRetry(retryAttempt)) continue; + throw new CompositeIOException(retryStrategy.failureMessage(path, retryAttempt), errors); + } + } + + public void forceRemoveRecursive(@Nonnull Path path) throws IOException { + for (int retryAttempt = 0; ; retryAttempt++) { + List errors = tryRemoveRecursive(path); + if (errors.isEmpty()) return; + if (retryStrategy.shouldRetry(retryAttempt)) continue; + throw new CompositeIOException(retryStrategy.failureMessage(path, retryAttempt), errors); + } + } + + @Restricted(NoExternalUse.class) + @FunctionalInterface + public interface PathChecker { + void check(@Nonnull Path path) throws SecurityException; + + PathChecker ALLOW_ALL = path -> {}; + } + + @Restricted(NoExternalUse.class) + @FunctionalInterface + public interface RetryStrategy { + boolean shouldRetry(int retriesAttempted); + + default String failureMessage(@Nonnull Path fileToRemove, int retryCount) { + StringBuilder sb = new StringBuilder() + .append("Unable to delete '") + .append(fileToRemove) + .append("'. Tried ") + .append(retryCount) + .append(" time"); + if (retryCount != 1) sb.append('s'); + sb.append('.'); + return sb.toString(); + } + } + + private static class PausingGCRetryStrategy implements RetryStrategy { + private final int maxRetries; + private final boolean gcAfterFailedRemove; + private final long waitBetweenRetries; + private final ThreadLocal interrupted = ThreadLocal.withInitial(() -> false); + + private PausingGCRetryStrategy(int maxRetries, boolean gcAfterFailedRemove, long waitBetweenRetries) { + this.maxRetries = maxRetries; + this.gcAfterFailedRemove = gcAfterFailedRemove; + this.waitBetweenRetries = waitBetweenRetries; + } + + @SuppressFBWarnings(value = "DM_GC", justification = "Garbage collection happens only when " + + "GC_AFTER_FAILED_DELETE is true. It's an experimental feature in Jenkins.") + private void gcIfEnabled() { + /* If the Jenkins process had the file open earlier, and it has not + * closed it then Windows won't let us delete it until the Java object + * with the open stream is Garbage Collected, which can result in builds + * failing due to "file in use" on Windows despite working perfectly + * well on other OSs. */ + if (gcAfterFailedRemove) System.gc(); + } + + @Override + public boolean shouldRetry(int retriesAttempted) { + if (retriesAttempted >= maxRetries) return false; + gcIfEnabled(); + long delayMillis = waitBetweenRetries >= 0 ? waitBetweenRetries : -(retriesAttempted + 1) * waitBetweenRetries; + if (delayMillis <= 0) return !Thread.interrupted(); + try { + Thread.sleep(delayMillis); + return true; + } catch (InterruptedException e) { + interrupted.set(true); + return false; + } + } + + @Override + public String failureMessage(@Nonnull Path fileToRemove, int retryCount) { + StringBuilder sb = new StringBuilder(); + sb.append("Unable to delete '"); + sb.append(fileToRemove); + sb.append("'. Tried "); + sb.append(retryCount + 1); + sb.append(" time"); + if (retryCount != 1) sb.append('s'); + if (maxRetries > 0) { + sb.append(" (of a maximum of "); + sb.append(maxRetries + 1); + sb.append(')'); + if (gcAfterFailedRemove) + sb.append(" garbage-collecting"); + if (waitBetweenRetries != 0 && gcAfterFailedRemove) + sb.append(" and"); + if (waitBetweenRetries != 0) { + sb.append(" waiting "); + sb.append(Util.getTimeSpanString(Math.abs(waitBetweenRetries))); + if (waitBetweenRetries < 0) { + sb.append("-"); + sb.append(Util.getTimeSpanString(Math.abs(waitBetweenRetries) * (maxRetries + 1))); + } + } + if (waitBetweenRetries != 0 || gcAfterFailedRemove) + sb.append(" between attempts"); + } + if (interrupted.get()) + sb.append(". The delete operation was interrupted before it completed successfully"); + sb.append('.'); + interrupted.set(false); + return sb.toString(); + } + } + + private Optional tryRemoveFile(@Nonnull Path path) { + try { + removeOrMakeRemovableThenRemove(path.normalize()); + return Optional.empty(); + } catch (IOException e) { + return Optional.of(e); + } + } + + private List tryRemoveRecursive(@Nonnull Path path) { + Path normalized = path.normalize(); + List accumulatedErrors = Util.isSymlink(normalized) ? new ArrayList<>() : + tryRemoveDirectoryContents(normalized); + tryRemoveFile(normalized).ifPresent(accumulatedErrors::add); + return accumulatedErrors; + } + + private List tryRemoveDirectoryContents(@Nonnull Path path) { + Path normalized = path.normalize(); + List accumulatedErrors = new ArrayList<>(); + if (!Files.isDirectory(normalized)) return accumulatedErrors; + try (DirectoryStream children = Files.newDirectoryStream(normalized)) { + for (Path child : children) { + accumulatedErrors.addAll(tryRemoveRecursive(child)); + } + } catch (IOException e) { + accumulatedErrors.add(e); + } + return accumulatedErrors; + } + + private void removeOrMakeRemovableThenRemove(@Nonnull Path path) throws IOException { + pathChecker.check(path); + try { + Files.deleteIfExists(path); + } catch (IOException e) { + makeRemovable(path); + try { + Files.deleteIfExists(path); + } catch (IOException e2) { + // see https://java.net/projects/hudson/lists/users/archive/2008-05/message/357 + // I suspect other processes putting files in this directory + if (Files.isDirectory(path)) { + List entries; + try (Stream children = Files.list(path)) { + entries = children.map(Path::toString).collect(Collectors.toList()); + } + throw new CompositeIOException("Unable to remove directory " + path + " with directory contents: " + entries, e, e2); + } + throw new CompositeIOException("Unable to remove file " + path, e, e2); + } + } + } + + private static void makeRemovable(@Nonnull Path path) throws IOException { + if (!Files.isWritable(path)) { + makeWritable(path); + } + /* + on Unix both the file and the directory that contains it has to be writable + for a file deletion to be successful. (Confirmed on Solaris 9) + + $ ls -la + total 6 + dr-xr-sr-x 2 hudson hudson 512 Apr 18 14:41 . + dr-xr-sr-x 3 hudson hudson 512 Apr 17 19:36 .. + -r--r--r-- 1 hudson hudson 469 Apr 17 19:36 manager.xml + -rw-r--r-- 1 hudson hudson 0 Apr 18 14:41 x + $ rm x + rm: x not removed: Permission denied + */ + Optional maybeParent = Optional.ofNullable(path.getParent()).map(Path::normalize).filter(p -> !Files.isWritable(p)); + if (maybeParent.isPresent()) { + makeWritable(maybeParent.get()); + } + } + + private static void makeWritable(@Nonnull Path path) throws IOException { + if (!Functions.isWindows()) { + try { + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + Set newPermissions = attrs.permissions(); + newPermissions.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(path, newPermissions); + } catch (NoSuchFileException ignored) { + return; + } catch (UnsupportedOperationException ignored) { + // PosixFileAttributes not supported, fall back to old IO. + } + } + + /* + * We intentionally do not check the return code of setWritable, because if it + * is false we prefer to rethrow the exception thrown by Files.deleteIfExists, + * which will have a more useful message than something we make up here. + */ + path.toFile().setWritable(true); + } + +} diff --git a/core/src/main/java/jenkins/util/java/JavaUtils.java b/core/src/main/java/jenkins/util/java/JavaUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..aa0e0084d0e03acbe40b7c10e1ea70b062bfe71f --- /dev/null +++ b/core/src/main/java/jenkins/util/java/JavaUtils.java @@ -0,0 +1,80 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.util.java; + +import hudson.util.VersionNumber; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Utility class for Java environment management and checks. + * @author Oleg Nenashev + */ +@Restricted(NoExternalUse.class) +public class JavaUtils { + + private JavaUtils() { + // Cannnot construct + } + + /** + * Check whether the current JVM is running with Java 8 or below + * @return {@code true} if it is Java 8 or older version + */ + public static boolean isRunningWithJava8OrBelow() { + String javaVersion = getCurrentRuntimeJavaVersion(); + return javaVersion.startsWith("1."); + } + + /** + * Check whether the current JVM is running with Java 9 or above. + * @return {@code true} if it is Java 9 or above + */ + public static boolean isRunningWithPostJava8() { + String javaVersion = getCurrentRuntimeJavaVersion(); + return !javaVersion.startsWith("1."); + } + + /** + * Returns the JVM's current version as a {@link VersionNumber} instance. + */ + public static VersionNumber getCurrentJavaRuntimeVersionNumber() { + return new VersionNumber(getCurrentRuntimeJavaVersion()); + } + + /** + * Returns the JVM's current version as a {@link String}. + * See https://openjdk.java.net/jeps/223 for the expected format. + *

    + *
  • Until Java 8 included, the expected format should be starting with 1.x
  • + *
  • Starting with Java 9, cf. JEP-223 linked above, the version got simplified in 9.x, 10.x, etc.
  • + *
+ * + * @see System#getProperty(String) + */ + public static String getCurrentRuntimeJavaVersion() { + // TODO: leverage Runtime.version() once on Java 9+ + return System.getProperty("java.specification.version"); + } +} diff --git a/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java b/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..972fb3251b84c0653b9c1cca5e599f8bb61b353d --- /dev/null +++ b/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java @@ -0,0 +1,55 @@ +/* + * The MIT License + * + * Copyright (c) 2018 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.util.xstream; + +import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.converters.basic.URLConverter; +import hudson.remoting.URLDeserializationHelper; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.IOException; +import java.net.URL; +import java.net.URLStreamHandler; + +/** + * Wrap the URL handler during deserialization into a specific one that does not generate DNS query on the hostname + * for {@link URLStreamHandler#equals(URL, URL)} or {@link URLStreamHandler#hashCode(URL)}. + * Required to protect against SECURITY-637 + * + * @since 2.121.3 + */ +@Restricted(NoExternalUse.class) +public class SafeURLConverter extends URLConverter { + + @Override + public Object fromString(String str) { + URL url = (URL) super.fromString(str); + try { + return URLDeserializationHelper.wrapIfRequired(url); + } catch (IOException e) { + throw new ConversionException(e); + } + } +} diff --git a/core/src/main/java/jenkins/util/xstream/XStreamDOM.java b/core/src/main/java/jenkins/util/xstream/XStreamDOM.java index d98f34654006178149dcdc90bb92068ce96116b2..2b2ed1621bd19511039532afd825b8327383e7ba 100644 --- a/core/src/main/java/jenkins/util/xstream/XStreamDOM.java +++ b/core/src/main/java/jenkins/util/xstream/XStreamDOM.java @@ -35,10 +35,10 @@ import com.thoughtworks.xstream.io.xml.AbstractXmlReader; import com.thoughtworks.xstream.io.xml.AbstractXmlWriter; import com.thoughtworks.xstream.io.xml.DocumentReader; import com.thoughtworks.xstream.io.xml.XmlFriendlyReplacer; -import com.thoughtworks.xstream.io.xml.Xpp3Driver; import hudson.Util; import hudson.util.VariableResolver; +import hudson.util.XStream2; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; @@ -241,11 +241,11 @@ public class XStreamDOM { * Writes this {@link XStreamDOM} into {@link OutputStream}. */ public void writeTo(OutputStream os) { - writeTo(new Xpp3Driver().createWriter(os)); + writeTo(XStream2.getDefaultDriver().createWriter(os)); } public void writeTo(Writer w) { - writeTo(new Xpp3Driver().createWriter(w)); + writeTo(XStream2.getDefaultDriver().createWriter(w)); } public void writeTo(HierarchicalStreamWriter w) { @@ -262,11 +262,11 @@ public class XStreamDOM { } public static XStreamDOM from(InputStream in) { - return from(new Xpp3Driver().createReader(in)); + return from(XStream2.getDefaultDriver().createReader(in)); } public static XStreamDOM from(Reader in) { - return from(new Xpp3Driver().createReader(in)); + return from(XStream2.getDefaultDriver().createReader(in)); } public static XStreamDOM from(HierarchicalStreamReader in) { diff --git a/core/src/main/java/jenkins/widgets/HistoryPageFilter.java b/core/src/main/java/jenkins/widgets/HistoryPageFilter.java index 269f5e44fc45ba5c3c718a1d9825ba9054adc19e..521ace8b791ecb09387fda38a6909cb4331c02c5 100644 --- a/core/src/main/java/jenkins/widgets/HistoryPageFilter.java +++ b/core/src/main/java/jenkins/widgets/HistoryPageFilter.java @@ -24,7 +24,6 @@ package jenkins.widgets; import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; import hudson.model.AbstractBuild; import hudson.model.Job; import hudson.model.ParameterValue; @@ -32,6 +31,7 @@ import hudson.model.ParametersAction; import hudson.model.Queue; import hudson.model.Run; import hudson.search.UserSearchProperty; +import hudson.util.Iterators; import hudson.widgets.HistoryWidget; import javax.annotation.Nonnull; diff --git a/core/src/main/resources/META-INF/upgrade/AccessControlled.hint b/core/src/main/resources/META-INF/upgrade/AccessControlled.hint new file mode 100644 index 0000000000000000000000000000000000000000..e3bafc391dfa419ed98a83f27ade596a6bdbcd13 --- /dev/null +++ b/core/src/main/resources/META-INF/upgrade/AccessControlled.hint @@ -0,0 +1 @@ +$ac.getACL().hasPermission($a, $p) :: $ac instanceof hudson.security.AccessControlled && $a instanceof org.acegisecurity.Authentication && $p instanceof hudson.security.Permission => $ac.hasPermission($a, $p);; diff --git a/core/src/main/resources/META-INF/upgrade/Hudson.hint b/core/src/main/resources/META-INF/upgrade/Hudson.hint index 4ff2df23fd1078997538baa071a0515624337f4f..5eb67cffce216fbcb3e4f0f6a6e08664c907991d 100644 --- a/core/src/main/resources/META-INF/upgrade/Hudson.hint +++ b/core/src/main/resources/META-INF/upgrade/Hudson.hint @@ -1 +1 @@ -hudson.model.Hudson.getInstance() => jenkins.model.Jenkins.getInstance();; +hudson.model.Hudson.getInstance() => jenkins.model.Jenkins.get();; diff --git a/core/src/main/resources/META-INF/upgrade/Items.hint b/core/src/main/resources/META-INF/upgrade/Items.hint new file mode 100644 index 0000000000000000000000000000000000000000..096e970106e21935563a1539208a3cdf13fdb2e3 --- /dev/null +++ b/core/src/main/resources/META-INF/upgrade/Items.hint @@ -0,0 +1,2 @@ +hudson.model.Items.getAllItems($root, $type) :: $root instanceof hudson.model.ItemGroup && $type instanceof java.lang.Class => $root.getAllItems($type);; +hudson.model.Items.allItems($root, $type) :: $root instanceof hudson.model.ItemGroup && $type instanceof java.lang.Class => $root.allItems($type);; diff --git a/core/src/main/resources/META-INF/upgrade/Jenkins.hint b/core/src/main/resources/META-INF/upgrade/Jenkins.hint new file mode 100644 index 0000000000000000000000000000000000000000..4e5821c5473d7412fdcf80cda2c0684b9aa05a51 --- /dev/null +++ b/core/src/main/resources/META-INF/upgrade/Jenkins.hint @@ -0,0 +1,2 @@ +jenkins.model.Jenkins.getInstance() => jenkins.model.Jenkins.get();; +jenkins.model.Jenkins.getActiveInstance() => jenkins.model.Jenkins.get();; diff --git a/core/src/main/resources/hudson/AboutJenkins/index.jelly b/core/src/main/resources/hudson/AboutJenkins/index.jelly index 68e063af6d83d2bd9905a3adf8bf13a898876458..51d7b090038a3adf34dd08de3b861b79e585095e 100644 --- a/core/src/main/resources/hudson/AboutJenkins/index.jelly +++ b/core/src/main/resources/hudson/AboutJenkins/index.jelly @@ -26,7 +26,7 @@ THE SOFTWARE. - +
@@ -35,7 +35,7 @@ THE SOFTWARE.

${%about(app.VERSION)}

${%blurb}

${%dependencies}

-

Mavenized dependencies

+

${%maven.dependencies}

diff --git a/core/src/main/resources/hudson/AboutJenkins/index.properties b/core/src/main/resources/hudson/AboutJenkins/index.properties index 33e9a29740aab4f45f185bd692c0af4e3f3d88b9..4f90f613f5f2a6316b7e2c59bc1a47d806d3a660 100644 --- a/core/src/main/resources/hudson/AboutJenkins/index.properties +++ b/core/src/main/resources/hudson/AboutJenkins/index.properties @@ -24,5 +24,6 @@ about=About Jenkins {0} blurb=Jenkins is a community-developed open-source automation server. dependencies=Jenkins depends on the following 3rd party libraries +maven.dependencies=Mavenized dependencies plugin.dependencies=License and dependency information for plugins static.dependencies=Static resources diff --git a/core/src/main/resources/hudson/AboutJenkins/index_bg.properties b/core/src/main/resources/hudson/AboutJenkins/index_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..81aeda52c0909f504916a7e676a936503f272430 --- /dev/null +++ b/core/src/main/resources/hudson/AboutJenkins/index_bg.properties @@ -0,0 +1,40 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, 2017, Alexander Shopov +# +# 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. + +# About Jenkins {0} +about=\ + \u041e\u0442\u043d\u043e\u0441\u043d\u043e Jenkins {0} +# Static resources +static.dependencies=\ + \u0421\u0442\u0430\u0442\u0438\u0447\u043d\u0438 \u0440\u0435\u0441\u0443\u0440\u0441\u0438 +# License and dependency information for plugins +plugin.dependencies=\ + \u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0442\u0435 \u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0438\u0442\u0435. +No\ information\ recorded=\ + \u041d\u0435 \u0441\u0435 \u0437\u0430\u043f\u0438\u0441\u0432\u0430 \u043d\u0438\u043a\u0430\u043a\u0432\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f +# Jenkins depends on the following 3rd party libraries +dependencies=\ + Jenkins \u0437\u0430\u0432\u0438\u0441\u0438 \u0438 \u043e\u0442 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043e\u0442 \u0434\u0440\u0443\u0433\u0438 \u043c\u0435\u0441\u0442\u0430 +# Jenkins is a community-developed open-source automation server. +blurb=\ + Jenkins \u0435 \u0441\u044a\u0440\u0432\u044a\u0440 \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u0441\ + \u043e\u0442\u0432\u043e\u0440\u0435\u043d \u043a\u043e\u0434, \u043a\u043e\u0439\u0442\u043e \u0441\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0432\u0430 \u043e\u0442 \u0441\u0432\u043e\u044f\u0442\u0430 \u043e\u0431\u0449\u043d\u043e\u0441\u0442 \u043e\u0442 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u0446\u0438. diff --git a/core/src/main/resources/hudson/AboutJenkins/index_it.properties b/core/src/main/resources/hudson/AboutJenkins/index_it.properties index 22a3db714d2d186c5d93f8a24c4652013260d440..e3041d1bb91503023ee149e520ddf85dca1f3207 100644 --- a/core/src/main/resources/hudson/AboutJenkins/index_it.properties +++ b/core/src/main/resources/hudson/AboutJenkins/index_it.properties @@ -1,5 +1,8 @@ -# This file is under the MIT License by authors - about=Informazioni su Jenkins {0} -blurb=Jenkins \u00E8 un server di "continuous integration" a codice aperto sviluppato da una comunit\u00E0. -dependencies=Jenkins dipende dalle seguenti librerie di terze parti. +blurb=Jenkins un server d''automazione open source sviluppato dalla comunit. + +dependencies=Jenkins dipende dalle seguenti librerie di terze parti +plugin.dependencies=Informazioni sulla licenza e sulle dipendenze per i plugin +static.dependencies=Risorse statiche +No\ information\ recorded=Nessun''informazione registrata +maven.dependencies=Dipendenze caricate tramite Maven diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 80ecb8db835f27d6f2e181080c3d696a8e15b1f5..235b5e3e093a67601431040c4a89e8c03128d454 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -63,6 +63,10 @@ PluginManager.ConfigureUpdateCenterPermission.Description=\ configure update sites and proxy settings. PluginManager.PluginCycleDependenciesMonitor.DisplayName=Cyclic Dependencies Detector PluginManager.PluginUpdateMonitor.DisplayName=Invalid Plugin Configuration +PluginManager.CheckUpdateServerError=There were errors checking the update sites: {0} +PluginManager.UpdateSiteError=Error checking update sites for {0} attempt(s). Last exception was: {1} +PluginManager.UpdateSiteChangeLogLevel=Change the log level of {0} logger to WARNING or below to see more information and the error message of every attempt +PluginManager.UnexpectedException=Unexpected exception going through the retrying process of checking update servers AboutJenkins.DisplayName=About Jenkins AboutJenkins.Description=See the version and license information. @@ -75,14 +79,19 @@ ProxyConfiguration.Success=Success Functions.NoExceptionDetails=No Exception details -PluginWrapper.missing={0} v{1} is missing. To fix, install v{1} or later. -PluginWrapper.failed_to_load_plugin={0} v{1} failed to load. -PluginWrapper.failed_to_load_dependency={0} v{1} failed to load. Fix this plugin first. -PluginWrapper.disabledAndObsolete={0} v{1} is disabled and older than required. To fix, install v{2} or later and enable it. +PluginWrapper.missing={0} version {1} is missing. To fix, install version {1} or later. +PluginWrapper.failed_to_load_plugin={0} version {1} failed to load. +PluginWrapper.failed_to_load_dependency={0} version {1} failed to load. Fix this plugin first. +PluginWrapper.disabledAndObsolete={0} version {1} is disabled and older than required. To fix, install version {2} or later and enable it. PluginWrapper.disabled={0} is disabled. To fix, enable it. -PluginWrapper.obsolete={0} v{1} is older than required. To fix, install v{2} or later. -PluginWrapper.obsoleteCore=You must update Jenkins from v{0} to v{1} or later to run this plugin. +PluginWrapper.obsolete={0} version {1} is older than required. To fix, install version {2} or later. +PluginWrapper.obsoleteCore=You must update Jenkins from version {0} to version {1} or later to run this plugin. +PluginWrapper.obsoleteJava=You must update Java from version {0} to version {1} or later to run this plugin. PluginWrapper.PluginWrapperAdministrativeMonitor.DisplayName=Plugins Failed To Load - +PluginWrapper.Already.Disabled=The plugin ''{0}'' was already disabled +PluginWrapper.Plugin.Has.Dependant=The plugin ''{0}'' has, at least, one dependant plugin ({1}) and the disable strategy is {2}, so it cannot be disabled +PluginWrapper.Plugin.Disabled=Plugin ''{0}'' disabled +PluginWrapper.NoSuchPlugin=No such plugin found with the name ''{0}'' +PluginWrapper.Error.Disabling=There was an error disabling the ''{0}'' plugin. Error: ''{1}'' TcpSlaveAgentListener.PingAgentProtocol.displayName=Ping protocol diff --git a/core/src/main/resources/hudson/Messages_bg.properties b/core/src/main/resources/hudson/Messages_bg.properties index 8791c38139914e2508d43dfdd623be1ba9a247cb..28e59f984cacfb7bf3edc985ad1bda9e3e08af3b 100644 --- a/core/src/main/resources/hudson/Messages_bg.properties +++ b/core/src/main/resources/hudson/Messages_bg.properties @@ -135,3 +135,15 @@ PluginWrapper.obsolete=\ # {0} v{1} failed to load. PluginWrapper.failed_to_load_plugin=\ \u201e{0}\u201c, \u0432\u0435\u0440\u0441\u0438\u044f {1} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0440\u0435\u0434\u0438. +# Plugins Failed To Load +PluginWrapper.PluginWrapperAdministrativeMonitor.DisplayName=\ + \u041f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0438\u0442\u0435 \u043d\u0435 \u0441\u0435 \u0437\u0430\u0440\u0435\u0434\u0438\u0445\u0430 +# Invalid Plugin Configuration +PluginManager.PluginUpdateMonitor.DisplayName=\ + \u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430 +# Whitespace can no longer be used as the separator. Please Use \u2018,\u2019 as the separator instead. +FilePath.validateAntFileMask.whitespaceSeparator=\ + \u0412\u0435\u0447\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0437\u0430 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b. \u041f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u043f\u0435\u0442\u0430\u044f: \u201e,\u201c +# Cyclic Dependencies Detector +PluginManager.PluginCycleDependenciesMonitor.DisplayName=\ + \u041e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0446\u0438\u043a\u043b\u0438\u0447\u043d\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 diff --git a/core/src/main/resources/hudson/Messages_es.properties b/core/src/main/resources/hudson/Messages_es.properties index 9ad1d7eeb3e00d25f1d4358510d148a6bc985d08..d7c9350298dae20b094a42ad6c3c8a7a75ca29be 100644 --- a/core/src/main/resources/hudson/Messages_es.properties +++ b/core/src/main/resources/hudson/Messages_es.properties @@ -23,13 +23,13 @@ FilePath.validateAntFileMask.whitespaceSeprator=\ No se pueden utilizar espacios en blanco como separadores. Utiliza coma '','' FilePath.validateAntFileMask.doesntMatchAndSuggest=\ - ''{0}'' no coincide con nada, pero ''{1}'' s. Quizas es lo que quieres decir. -FilePath.validateAntFileMask.portionMatchAndSuggest=''{0}'' no coincide con nada aunque ''{1}'' s existe + ''{0}'' no coincide con nada, pero ''{1}'' s\u00ED. Quizas es lo que quieres decir. +FilePath.validateAntFileMask.portionMatchAndSuggest=''{0}'' no coincide con nada aunque ''{1}'' s\u00ED existe FilePath.validateAntFileMask.portionMatchButPreviousNotMatchAndSuggest=''{0}'' no coincide con nada: ''{1}'' existe, pero ''{2}'' no existe FilePath.validateAntFileMask.doesntMatchAnything=''{0}'' no coincide con nada FilePath.validateAntFileMask.doesntMatchAnythingAndSuggest=''{0}'' no coincide con nada: ''{1}'' no existe -FilePath.validateRelativePath.wildcardNotAllowed=No se permiten caracteres comodn. +FilePath.validateRelativePath.wildcardNotAllowed=No se permiten caracteres comod\u00EDn. FilePath.validateRelativePath.notFile=''{0}'' no es un archivo FilePath.validateRelativePath.notDirectory=''{0}'' no es un directorio FilePath.validateRelativePath.noSuchFile=El fichero no existe ''{0}'' @@ -39,19 +39,27 @@ Util.millisecond={0} Ms Util.second={0} Seg Util.minute={0} Min Util.hour ={0} Hor -Util.day ={0} {0,choice,0#das|1#da|1 -
- ${%PluginCycles} -
    - -
  • -
    -
+
+
+
${%PluginCycles}
+ +
+
+
diff --git a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.properties b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.properties index 7a9748d28f4ce073fd44ffd2c2755236bb255a93..0bfeb5b97cc0ea3d09421d0de2274883f3fa0a8e 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.properties +++ b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.properties @@ -21,4 +21,4 @@ # THE SOFTWARE. -PluginCycles=The following plugins are deactivated because of cyclic dependencies, most likely you can resolve the issue by updating these to a newer version. \ No newline at end of file +PluginCycles=The following plugins are deactivated because of cyclic dependencies, most likely you can resolve the issue by updating these to a newer version \ No newline at end of file diff --git a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message_it.properties b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..38252a52fcaeebcab4cd8d80528ca645f135d9b0 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message_it.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# 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. + +PluginCycles=I seguenti plugin sono disattivati a causa di dipendenze cicliche, molto probabilmente possibile risolvere il problema aggiornandoli a una versione pi recente. diff --git a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly index 66093c4fc02c90d413850961f5e7bee5e68122c8..73182ec483e4091494004eaf5787f50ccd96889b 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly @@ -24,12 +24,12 @@ THE SOFTWARE. -
- ${%RequiredPluginUpdates} -
    - -
  • -
    -
+
+
+
${%RequiredPluginUpdates}
+ +
+
+
diff --git a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.properties b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.properties index 930492c7359794667b81bcdf12896da11a9f3be8..b40f9a2956ba1b860234e4b26c4f50ba70b514e9 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.properties +++ b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.properties @@ -21,4 +21,4 @@ # THE SOFTWARE. -RequiredPluginUpdates=The following plugins require an update. \ No newline at end of file +RequiredPluginUpdates=The following plugins require an update \ No newline at end of file diff --git a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message_it.properties b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..21e4074d3ed9c489f4b9c327897914a5f64935e0 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message_it.properties @@ -0,0 +1,23 @@ +# The MIT License +# +# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# 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. + +RequiredPluginUpdates=I seguenti plugin richiedono un aggiornamento. diff --git a/core/src/main/resources/hudson/PluginManager/_api.jelly b/core/src/main/resources/hudson/PluginManager/_api.jelly index 8ef92ccf32d05e83b271f61eef04705ec6a631aa..6c802e8a809790ce37363201acf3d9a19739712c 100644 --- a/core/src/main/resources/hudson/PluginManager/_api.jelly +++ b/core/src/main/resources/hudson/PluginManager/_api.jelly @@ -51,7 +51,7 @@ Content-Type: application/javascript; charset=UTF-8 ]

- Whereas prevalidateConfig call runs without any side-effect, you can also + Whereas prevalidateConfig call runs without any side-effect, you can also submit the same XML to installNecessaryPlugins endpoint to install missing plugins and updating old ones. This calls returns immediately after initiating plugin installation process, without waiting for the completion diff --git a/core/src/main/resources/hudson/PluginManager/_table.css b/core/src/main/resources/hudson/PluginManager/_table.css index 86125969bb52a898599f74c6fbe0fd17edeed6be..9107c2be8bc33bab488765f49433e6395be3d52c 100644 --- a/core/src/main/resources/hudson/PluginManager/_table.css +++ b/core/src/main/resources/hudson/PluginManager/_table.css @@ -8,3 +8,6 @@ margin: 1em; text-align: right; } +.compatWarning, .securityWarning { + font-weight: bold; +} diff --git a/core/src/main/resources/hudson/PluginManager/_table.js b/core/src/main/resources/hudson/PluginManager/_table.js index 417694036293526252eafc48b68286e33f207404..845185ef6dd1114ede5d7ad45157715fc5dd3644 100644 --- a/core/src/main/resources/hudson/PluginManager/_table.js +++ b/core/src/main/resources/hudson/PluginManager/_table.js @@ -1,15 +1,24 @@ +function checkPluginsWithoutWarnings() { + var inputs = document.getElementsByTagName('input'); + for(var i = 0; i < inputs.length; i++) { + var candidate = inputs[i]; + if(candidate.type === "checkbox" && candidate.dataset.compatWarning === 'false') { + candidate.checked = true; + } + } +} function showhideCategories(hdr,on) { var table = hdr.parentNode.parentNode.parentNode, newDisplay = on ? '' : 'none', - nameList = new Array(), name; + nameList = new Array(), id; for (var i = 1; i < table.rows.length; i++) { if (on || table.rows[i].cells.length == 1) table.rows[i].style.display = newDisplay; else { - // Hide duplicate rows for a plugin when not viewing by-category - name = table.rows[i].cells[1].getAttribute('data'); - if (nameList[name] == 1) table.rows[i].style.display = 'none'; - nameList[name] = 1; + // Hide duplicate rows for a plugin:version when not viewing by-category + id = table.rows[i].cells[1].getAttribute('data-id'); + if (nameList[id] == 1) table.rows[i].style.display = 'none'; + nameList[id] = 1; } } } @@ -28,7 +37,9 @@ Behaviour.specify("#filter-box", '_table', 0, function(e) { var items = document.getElementsBySelector(clz); for (var i=0; i=0); - var name = items[i].getAttribute("name"); + var name = items[i].cells && items[i].cells.length > 1 + ? items[i].cells[1].getAttribute('data-id') + : items[i].getAttribute("name"); if (visible && name != null) { if (encountered[name]) { visible = false; @@ -397,4 +408,4 @@ Behaviour.specify("#filter-box", '_table', 0, function(e) { setEnableWidgetStates(); }); -}()); \ No newline at end of file +}()); diff --git a/core/src/main/resources/hudson/PluginManager/advanced.jelly b/core/src/main/resources/hudson/PluginManager/advanced.jelly index 362d28e9ca87abe5ca7e6a8642f23e302231ee18..9c06363f035a2cbb8f2e58e04c86905ae0e32f1a 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced.jelly +++ b/core/src/main/resources/hudson/PluginManager/advanced.jelly @@ -44,7 +44,7 @@ THE SOFTWARE. - + @@ -73,7 +73,7 @@ THE SOFTWARE. - + diff --git a/core/src/main/resources/hudson/PluginManager/advanced_bg.properties b/core/src/main/resources/hudson/PluginManager/advanced_bg.properties index 60ac6803dba9e50d0cc251ad6a7161c6e1173b2d..0fb4aa2df67dbb390cd9f3461de653f8163de9a7 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_bg.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_bg.properties @@ -20,23 +20,22 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -File=\u0424\u0430\u0439\u043b +File=\ + \u0424\u0430\u0439\u043B HTTP\ Proxy\ Configuration=\ - \u0421\u044a\u0440\u0432\u044a\u0440-\u043f\u043e\u0441\u0440\u0435\u0434\u043d\u0438\u043a \u0437\u0430 HTTP -Submit=\ - \u041f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0430\u0432\u0430\u043d\u0435 + \u0421\u044A\u0440\u0432\u044A\u0440-\u043F\u043E\u0441\u0440\u0435\u0434\u043D\u0438\u043A \u0437\u0430 HTTP Update\ Site=\ - \u041e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442\u0430 + \u041E\u0431\u043D\u043E\u0432\u044F\u0432\u0430\u043D\u0435 \u043D\u0430 \u0441\u0430\u0439\u0442\u0430 Upload=\ - \u041a\u0430\u0447\u0432\u0430\u043d\u0435 + \u041A\u0430\u0447\u0432\u0430\u043D\u0435 Upload\ Plugin=\ - \u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430 + \u041A\u0430\u0447\u0432\u0430\u043D\u0435 \u043D\u0430 \u043F\u0440\u0438\u0441\u0442\u0430\u0432\u043A\u0430 uploadtext=\ - \u041c\u043e\u0436\u0435 \u0434\u0430 \u043a\u0430\u0447\u0438\u0442\u0435 \u0444\u0430\u0439\u043b \u0432\u044a\u0432 \u0444\u043e\u0440\u043c\u0430\u0442 \u201e.hpi\u201c, \u0437\u0430 \u0434\u0430 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430 \u0438\u0437\u0432\u044a\u043d\ - \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u043d\u043e\u0442\u043e \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. + \u041C\u043E\u0436\u0435 \u0434\u0430 \u043A\u0430\u0447\u0438\u0442\u0435 \u0444\u0430\u0439\u043B \u0432\u044A\u0432 \u0444\u043E\u0440\u043C\u0430\u0442 \u201E.hpi\u201C, \u0437\u0430 \u0434\u0430 \u0438\u043D\u0441\u0442\u0430\u043B\u0438\u0440\u0430\u0442\u0435 \u043F\u0440\u0438\u0441\u0442\u0430\u0432\u043A\u0430 \u0438\u0437\u0432\u044A\u043D\ + \u043E\u0444\u0438\u0446\u0438\u0430\u043B\u043D\u043E\u0442\u043E \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435. Other\ Sites=\ - \u0414\u0440\u0443\u0433\u0438 \u0441\u0430\u0439\u0442\u043e\u0432\u0435 + \u0414\u0440\u0443\u0433\u0438 \u0441\u0430\u0439\u0442\u043E\u0432\u0435 Update\ Center=\ - \u0421\u0430\u0439\u0442 \u0437\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f + \u0421\u0430\u0439\u0442 \u0437\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F URL=\ \u0410\u0434\u0440\u0435\u0441 diff --git a/core/src/main/resources/hudson/PluginManager/advanced_cs.properties b/core/src/main/resources/hudson/PluginManager/advanced_cs.properties index 4551fc765436b07c2e6aa60b370ef2dfd19e9f35..1d0183d7438feb2052b41e448cb506eac6be7297 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_cs.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_cs.properties @@ -2,7 +2,6 @@ File=Soubor HTTP\ Proxy\ Configuration=Nastaven\u00ED HTTP Proxy -Submit=Odeslat Update\ Site=\u00DAlo\u017Ei\u0161t\u011B aktualizac\u00ED Upload=Nahr\u00E1t Upload\ Plugin=Nahr\u00E1t Plugin diff --git a/core/src/main/resources/hudson/PluginManager/advanced_da.properties b/core/src/main/resources/hudson/PluginManager/advanced_da.properties index cea2ce412ded480e5ccc630d9e2a1ff3cee6a603..6d149c87fcf9bc7c6b13d0cb707c7fca3b7d4ce4 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_da.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_da.properties @@ -22,14 +22,13 @@ Update\ Site=Opdateringssite Password=Adgangskode -Upload=L\u00e6g op +Upload=L\u00E6g op User\ name=Brugernavn File=Fil URL=URL lastUpdated=Opdateringsinformation hentet: {0} dage siden -uploadtext=Du kan l\u00e6gge en .hpi fil op for at installere en plugin fra udenfor det centrale plugin lager. +uploadtext=Du kan l\u00E6gge en .hpi fil op for at installere en plugin fra udenfor det centrale plugin lager. Port=Port HTTP\ Proxy\ Configuration=HTTP proxykonfiguration Server=Server -Upload\ Plugin=L\u00e6g plugin op -Submit=Gem +Upload\ Plugin=L\u00E6g plugin op diff --git a/core/src/main/resources/hudson/PluginManager/advanced_de.properties b/core/src/main/resources/hudson/PluginManager/advanced_de.properties index d5b7ed5f20bfb5e6477167d17bb09723c19fe9f7..d38b9767fed6db662fc5f7f31dc9b06bd7498f25 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_de.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_de.properties @@ -21,7 +21,6 @@ # THE SOFTWARE. HTTP\ Proxy\ Configuration=HTTP-Proxy Konfiguration -Submit=bernehmen Upload\ Plugin=Plugin hochladen File=Datei Upload=Hochladen diff --git a/core/src/main/resources/hudson/PluginManager/advanced_es.properties b/core/src/main/resources/hudson/PluginManager/advanced_es.properties index 1a5ebcf151b3e6748deb957f44fd5e3ca60986f5..881f9874cd088fad10f928e7b54d89d3bd28564c 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_es.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_es.properties @@ -20,14 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -lastUpdated=Informacion de actualizacin es de hace {0}. +lastUpdated=Informacion de actualizaci\uFFFDn es de hace {0}. uploadtext=\ Puedes subir un fichero .hpi para instalar un plugin que no este en el repositorio central. -Submit=Enviar File=Archivo Upload\ Plugin=Subir un plugin Upload=Subir -HTTP\ Proxy\ Configuration=Configuracin de proxy -Update\ Site=Direccin para la actualizacin +HTTP\ Proxy\ Configuration=Configuraci\uFFFDn de proxy +Update\ Site=Direcci\uFFFDn para la actualizaci\uFFFDn URL=Url Update\ Center=Centro de actualizaciones diff --git a/core/src/main/resources/hudson/PluginManager/advanced_fi.properties b/core/src/main/resources/hudson/PluginManager/advanced_fi.properties index e406d3c570ddd905b3432e2bc388896c158df316..6604dfe06cdfc92b3c3a83cee6b204d9766a8f0f 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_fi.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_fi.properties @@ -25,7 +25,6 @@ HTTP\ Proxy\ Configuration=HTTP-v\u00E4lipalvelinasetukset Password=Salasana Port=Portti Server=Palvelin -Submit=L\u00E4het\u00E4 URL=URL Update\ Site=P\u00E4ivityssivusto Upload=Lataa diff --git a/core/src/main/resources/hudson/PluginManager/advanced_fr.properties b/core/src/main/resources/hudson/PluginManager/advanced_fr.properties index 85763f7c7b395ce67524abcb104bcb609e775ff9..7e9c382c2872030abafc6abe384a4e37cb12646d 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_fr.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_fr.properties @@ -21,7 +21,6 @@ # THE SOFTWARE. HTTP\ Proxy\ Configuration=Configuration du proxy HTTP -Submit=Soumettre Upload\ Plugin=Soumettre un plugin File=Fichier Update\ Site=Site de mise \u00E0 jour diff --git a/core/src/main/resources/hudson/PluginManager/advanced_hu.properties b/core/src/main/resources/hudson/PluginManager/advanced_hu.properties index 01e7d77e9e2976d4773d64bcd43cc799bd857c2e..23f4e734192dfe55c6c601fee16266b12bb12e13 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_hu.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_hu.properties @@ -2,7 +2,6 @@ File=\u00C1llom\u00E1ny HTTP\ Proxy\ Configuration=HTTP Proxy Be\u00E1ll\u00EDt\u00E1sok -Submit=Elk\u00FCld Update\ Site=Friss\u00EDt\u00E9si Oldal Upload=Felt\u00F6lt Upload\ Plugin=Be\u00E9p\u00FCl\u0151 Felt\u00F6lt\u00E9se diff --git a/core/src/main/resources/hudson/PluginManager/advanced_it.properties b/core/src/main/resources/hudson/PluginManager/advanced_it.properties index aba4a0d6df08b580264bcf1e8f681f9d12fe3e5f..31a9263d9cfe36274401b19cce3bff1deeda87ed 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_it.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_it.properties @@ -20,13 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -HTTP\ Proxy\ Configuration=Configurazione Proxy HTTP -Port=Porta -Submit=Invia -URL=Indirizzo URL +HTTP\ Proxy\ Configuration=Configurazione proxy HTTP +URL=URL Update\ Site=Aggiorna sito Upload=Carica -Upload\ Plugin=Carica estensione -User\ name=Nome utente -lastUpdated=Informazioni di aggiornamento ottenute: {0} fa -uploadtext=Puoi fare l''upload di un file .hpi per installare +Upload\ Plugin=Carica plugin +uploadtext=\uFFFD possibile caricare un file .hpi per installare un plugin da una fonte esterna al repository plugin centrale. +Update\ Center=Centro aggiornamenti +File=File +Other\ Sites=Altri siti diff --git a/core/src/main/resources/hudson/PluginManager/advanced_ja.properties b/core/src/main/resources/hudson/PluginManager/advanced_ja.properties index 149795c96008d2076a5051e1ff3d4d8d1238e2d1..2f2ea0932c7feecad1317cb2f6176aa0e0d7cbe4 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_ja.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_ja.properties @@ -20,14 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -HTTP\ Proxy\ Configuration=HTTP Proxy\u306e\u8a2d\u5b9a -Submit=\u4fdd\u5b58 -Upload\ Plugin=\u30d7\u30e9\u30b0\u30a4\u30f3\u306e\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 -Upload=\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 -File=\u30d5\u30a1\u30a4\u30eb +HTTP\ Proxy\ Configuration=HTTP Proxy\u306E\u8A2D\u5B9A +Upload\ Plugin=\u30D7\u30E9\u30B0\u30A4\u30F3\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9 +Upload=\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9 +File=\u30D5\u30A1\u30A4\u30EB uploadtext=\ - .hpi\u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3057\u3066\u3001\u30d7\u30e9\u30b0\u30a4\u30f3\u30ea\u30dd\u30b8\u30c8\u30ea\u4ee5\u5916\u304b\u3089\u30d7\u30e9\u30b0\u30a4\u30f3\u3092\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u304d\u307e\u3059\u3002 -Update\ Site=\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30b5\u30a4\u30c8 + .hpi\u30D5\u30A1\u30A4\u30EB\u3092\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3057\u3066\u3001\u30D7\u30E9\u30B0\u30A4\u30F3\u30EA\u30DD\u30B8\u30C8\u30EA\u4EE5\u5916\u304B\u3089\u30D7\u30E9\u30B0\u30A4\u30F3\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3067\u304D\u307E\u3059\u3002 +Update\ Site=\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8\u30B5\u30A4\u30C8 URL=URL -Update\ Center=\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30bb\u30f3\u30bf\u30fc -Other\ Sites=\u4ed6\u30b5\u30a4\u30c8 +Update\ Center=\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8\u30BB\u30F3\u30BF\u30FC +Other\ Sites=\u4ED6\u30B5\u30A4\u30C8 diff --git a/core/src/main/resources/hudson/PluginManager/advanced_ko.properties b/core/src/main/resources/hudson/PluginManager/advanced_ko.properties index 18b829d7bdeae58b2272d17ae0a6dc42a3b154df..d1146c49661a03fda97b74f76c71f7945770b074 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_ko.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_ko.properties @@ -25,7 +25,6 @@ HTTP\ Proxy\ Configuration=HTTP \uD504\uB85D\uC2DC \uC124\uC815 Password=\uC554\uD638 Port=\uD3EC\uD2B8 Server=\uC11C\uBC84 -Submit=\uC800\uC7A5 URL=\uC0AC\uC774\uD2B8\uACBD\uB85C Update\ Site=\uC5C5\uB370\uC774\uD2B8 \uC0AC\uC774\uD2B8 Upload=\uC62C\uB9AC\uAE30 diff --git a/core/src/main/resources/hudson/PluginManager/advanced_lv.properties b/core/src/main/resources/hudson/PluginManager/advanced_lv.properties index 3e103e2a01c141ee0988a7633386abfd8b774e87..c5e1bac0032fe9a14dbd03957a75eb7ce9727155 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_lv.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_lv.properties @@ -2,7 +2,6 @@ File=Fails HTTP\ Proxy\ Configuration=HTTP Starpniekservera konfigur\u0101cija -Submit=Nos\u016Bt\u012Bt Update\ Site=Aug\u0161upl\u0101des vietne Upload=Aug\u0161upl\u0101d\u0113t Upload\ Plugin=Aug\u0161upl\u0101d\u0113t spraudni diff --git a/core/src/main/resources/hudson/PluginManager/advanced_nb_NO.properties b/core/src/main/resources/hudson/PluginManager/advanced_nb_NO.properties index 5cec6ec57d6638dd012f69973dc516c5caf58695..2ac5d17c50d1124e88fa03252beb233fe3220440 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_nb_NO.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_nb_NO.properties @@ -25,7 +25,6 @@ HTTP\ Proxy\ Configuration=HTTP mellomtjener konfigurasjon Password=Passord Port=Port Server=Server -Submit=Lagre Upload=Last opp Upload\ Plugin=Last opp programtillegg User\ name=Brukernavn diff --git a/core/src/main/resources/hudson/PluginManager/advanced_nl.properties b/core/src/main/resources/hudson/PluginManager/advanced_nl.properties index eddf5de9983b729c25567f112c4179e17e5d7409..f082f09c983dc186ac6218fd6b499fd12f717b84 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_nl.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_nl.properties @@ -29,6 +29,5 @@ User\ name=Gebruikersnaam URL=URL Upload=Uploaden HTTP\ Proxy\ Configuration=HTTP-proxyconfiguratie -Submit=Versturen Upload\ Plugin=Plugin uploaden File=Bestand diff --git a/core/src/main/resources/hudson/PluginManager/advanced_pl.properties b/core/src/main/resources/hudson/PluginManager/advanced_pl.properties index d930415d3111339612c82b1e4cc7fd80758c5e70..2bd68cd86105ca5dbc889dfc5b4b87eb109c1e63 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_pl.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_pl.properties @@ -22,7 +22,6 @@ File=Plik HTTP\ Proxy\ Configuration=Konfiguracja HTTP Proxy -Submit=Prze\u015Blij URL=Adres URL Update\ Site=Strona z aktualizacjami Upload=Prze\u015Blij diff --git a/core/src/main/resources/hudson/PluginManager/advanced_pt_BR.properties b/core/src/main/resources/hudson/PluginManager/advanced_pt_BR.properties index 6de0e5b3beaf545c65ebc05d0928b37226063585..27f20b4e17a5101d61d40e84c3dc5a558f89ea95 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_pt_BR.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_pt_BR.properties @@ -20,14 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -lastUpdated=Informa\u00e7\u00e3o de atualiza\u00e7\u00e3o obtida: {0} atr\u00e1s +lastUpdated=Informa\u00E7\u00E3o de atualiza\u00E7\u00E3o obtida: {0} atr\u00E1s uploadtext=Voc\u00EA pode fazer o upload de um arquivo .hpi para instalar um plugin fora do reposit\u00F3rio central. -Update\ Site=Site de atualiza\u00e7\u00e3o +Update\ Site=Site de atualiza\u00E7\u00E3o File=Arquivo Upload\ Plugin=Atualizar plugin -HTTP\ Proxy\ Configuration=Configura\u00e7\u00e3o de Proxy/HTTP +HTTP\ Proxy\ Configuration=Configura\u00E7\u00E3o de Proxy/HTTP URL=URL Upload=Upload -Submit=Enviar -Update\ Center=Central de atualiza\u00e7\u00f5es +Update\ Center=Central de atualiza\u00E7\u00F5es Other\ Sites=Outros sites diff --git a/core/src/main/resources/hudson/PluginManager/advanced_pt_PT.properties b/core/src/main/resources/hudson/PluginManager/advanced_pt_PT.properties index 5f2a622c91561abba8a4ec39542aaed39da1acfd..e3be3112eb1f143c1f02d8d1abbbb9a1e4bc110a 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_pt_PT.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_pt_PT.properties @@ -1,7 +1,6 @@ # This file is under the MIT License by authors File=Ficheiro -Submit=Enviar Update\ Site=Atualizar Site Upload=Enviar Upload\ Plugin=Enviar Plugin diff --git a/core/src/main/resources/hudson/PluginManager/advanced_ru.properties b/core/src/main/resources/hudson/PluginManager/advanced_ru.properties index 98ef9a67d9c0477a39df396e461ec66aa4be75db..c04a6fe862f4287f62d4705669afeebb66ee4502 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_ru.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_ru.properties @@ -25,7 +25,6 @@ HTTP\ Proxy\ Configuration=\u041A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u043 Password=\u041F\u0430\u0440\u043E\u043B\u044C Port=\u041F\u043E\u0440\u0442 Server=\u0421\u0435\u0440\u0432\u0435\u0440 -Submit=\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C URL=\u0410\u0434\u0440\u0435\u0441 URL Update\ Site=\u0421\u0430\u0439\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0439 Upload=\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C diff --git a/core/src/main/resources/hudson/PluginManager/advanced_sk.properties b/core/src/main/resources/hudson/PluginManager/advanced_sk.properties index 2b35c4933a515d7e1a8b049ad9dc59b0dd75f574..5fda294bb9a2fcd39178cb44bc6b69d087d9b1eb 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_sk.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_sk.properties @@ -2,5 +2,4 @@ File=S\u00FAbor HTTP\ Proxy\ Configuration=Konfigur\u00E1cia HTTP proxy -Submit=Po\u0161li lastUpdated=Inform\u00E1cia o aktualiz\u00E1ci\u00E1ch z\u00EDskan\u00E1 pred: {0} diff --git a/core/src/main/resources/hudson/PluginManager/advanced_sr.properties b/core/src/main/resources/hudson/PluginManager/advanced_sr.properties index 99f4d1a1e12f373202f7ccda4f029501ddd896d6..ec453b66ce17bfd52c66544f2a6877dc1b7b43bf 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_sr.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_sr.properties @@ -2,7 +2,6 @@ Update\ Center=\u0426\u0435\u043D\u0442\u0430\u0440 \u0437\u0430 \u0430\u0436\u0443\u0440\u0438\u0440\u0430\u045A\u0435 HTTP\ Proxy\ Configuration=\u041F\u043E\u0441\u0442\u0430\u0432\u0459\u0430\u045A\u0435 HTTP Proxy -Submit=\u041F\u043E\u0442\u0432\u0440\u0434\u0438 Upload\ Plugin=\u041E\u0442\u043F\u0440\u0435\u043C\u0438 \u043C\u043E\u0434\u0443\u043B\u0443 uploadtext=\u041C\u043E\u0436\u0435\u0442\u0435 \u043E\u0442\u043F\u0440\u0435\u043C\u0438\u0442\u0438 \u0434\u0430\u0442\u043E\u0442\u0435\u043A\u0443 \u0443 \u0444\u043E\u0440\u043C\u0430\u0442\u0443 ".hpi", \u0434\u0430 \u0431\u0438\u0441\u0442\u0435 \u0438\u043D\u0441\u0442\u0430\u043B\u0438\u0440\u0430\u043B\u0438 \u043C\u043E\u0434\u0443\u043B\u0443 \u0432\u0430\u043D\ \u0446\u0435\u043D\u0442\u0440\u0430\u043B\u043D\u043E\u0433 \u0438\u0437\u0432\u043E\u0440\u0430 \u0437\u0430 \u043C\u043E\u0434\u0443\u043B\u0435. diff --git a/core/src/main/resources/hudson/PluginManager/advanced_sv_SE.properties b/core/src/main/resources/hudson/PluginManager/advanced_sv_SE.properties index 10c631c4425c4fc8be151e753f48e622baa37b1f..7c6bae6e81a76fe5e1a603d08f365bf8196dbf97 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_sv_SE.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_sv_SE.properties @@ -25,7 +25,6 @@ HTTP\ Proxy\ Configuration=HTTP-proxykonfiguration Password=L\u00F6senord Port=Port Server=Server -Submit=Skicka URL=URL Update\ Site=Updateringssajt Upload=Ladda upp diff --git a/core/src/main/resources/hudson/PluginManager/advanced_tr.properties b/core/src/main/resources/hudson/PluginManager/advanced_tr.properties index daf9bed295c36d36e3065814568f7c2be3606f62..34bd395fc3b11367a80fa1391e2c8d919d7427e6 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_tr.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_tr.properties @@ -20,12 +20,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -HTTP\ Proxy\ Configuration=HTTP Proxy Konfig\u00fcrasyonu -Submit=G\u00f6nder -Upload\ Plugin=Eklenti Y\u00fckle +HTTP\ Proxy\ Configuration=HTTP Proxy Konfig\u00FCrasyonu +Upload\ Plugin=Eklenti Y\u00FCkle uploadtext=\ -Merkezi eklenti repository''si d\u0131\u015f\u0131nda bir eklenti eklemek i\u00e7in .hpi dosyas\u0131n\u0131 y\u00fcklemeniz yeterli olacakt\u0131r. +Merkezi eklenti repository''si d\u0131\u015F\u0131nda bir eklenti eklemek i\u00E7in .hpi dosyas\u0131n\u0131 y\u00FCklemeniz yeterli olacakt\u0131r. File=Dosya Update\ Site=G\u00FCncelleme sitesi -Upload=Y\u00fckle -lastUpdated=Al\u0131nan son g\u00fcncelleme bilgisi : {0} \u00f6nce +Upload=Y\u00FCkle +lastUpdated=Al\u0131nan son g\u00FCncelleme bilgisi : {0} \u00F6nce diff --git a/core/src/main/resources/hudson/PluginManager/advanced_zh_TW.properties b/core/src/main/resources/hudson/PluginManager/advanced_zh_TW.properties index 299d2013bc0a59afa01bc775deaec8a0c73d3ff9..5a6a9c1420a9a3e4c19b72924ab5b4a5acd9e65a 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced_zh_TW.properties +++ b/core/src/main/resources/hudson/PluginManager/advanced_zh_TW.properties @@ -21,18 +21,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Update\ Center=\u66f4\u65b0\u4e2d\u5fc3 +Update\ Center=\u66F4\u65B0\u4E2D\u5FC3 HTTP\ Proxy\ Configuration=HTTP Proxy\u8A2D\u5B9A -Submit=\u9001\u51fa -Upload\ Plugin=\u4e0a\u50b3\u5916\u639b\u7a0b\u5f0f -uploadtext=\u60a8\u53ef\u4ee5\u624b\u52d5\u4e0a\u50b3 .hpi \u6a94\u6848\u4f86\u5b89\u88dd\u4e0d\u5728\u4e2d\u592e\u5132\u5b58\u5eab\u4e0a\u7684\u5916\u639b\u7a0b\u5f0f\u3002 -File=\u6a94\u6848 -Upload=\u4e0a\u50b3 +Upload\ Plugin=\u4E0A\u50B3\u5916\u639B\u7A0B\u5F0F +uploadtext=\u60A8\u53EF\u4EE5\u624B\u52D5\u4E0A\u50B3 .hpi \u6A94\u6848\u4F86\u5B89\u88DD\u4E0D\u5728\u4E2D\u592E\u5132\u5B58\u5EAB\u4E0A\u7684\u5916\u639B\u7A0B\u5F0F\u3002 +File=\u6A94\u6848 +Upload=\u4E0A\u50B3 Update\ Site=\u66F4\u65B0\u7DB2\u5740 URL=URL -Other\ Sites=\u5176\u4ed6\u7db2\u7ad9 -lastUpdated=\u66f4\u65b0\u8cc7\u8a0a\u53d6\u5f97\u6642\u9593: {0}\u4ee5\u524d +Other\ Sites=\u5176\u4ED6\u7DB2\u7AD9 +lastUpdated=\u66F4\u65B0\u8CC7\u8A0A\u53D6\u5F97\u6642\u9593: {0}\u4EE5\u524D diff --git a/core/src/main/resources/hudson/PluginManager/available_nl.properties b/core/src/main/resources/hudson/PluginManager/available_nl.properties index d40785c36550313ba4a6d1b8381efb78793965ee..5951113a3c49252579ad53cf757ac1be0c067620 100644 --- a/core/src/main/resources/hudson/PluginManager/available_nl.properties +++ b/core/src/main/resources/hudson/PluginManager/available_nl.properties @@ -20,6 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Updates=Nieuwere versies +Updates=Updates Available=Beschikbaar Installed=Ge\u00EFnstalleerd diff --git a/core/src/main/resources/hudson/PluginManager/check.jelly b/core/src/main/resources/hudson/PluginManager/check.jelly index 2270efaa512b2b51ce76ba815d81508b1831e982..be77c779433f9e09725e6e252ad44c52bf58b1df 100644 --- a/core/src/main/resources/hudson/PluginManager/check.jelly +++ b/core/src/main/resources/hudson/PluginManager/check.jelly @@ -29,5 +29,9 @@ THE SOFTWARE. - + +

+ ${app.pluginManager.lastErrorCheckUpdateCenters} +
+ diff --git a/core/src/main/resources/hudson/PluginManager/checkUpdates_it.properties b/core/src/main/resources/hudson/PluginManager/checkUpdates_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..9bd80d3b77e17e5b17a1ee55d51704b7cbec8034 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/checkUpdates_it.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# 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. + +Checking\ Updates...=Controllo aggiornamenti in corso... +Update\ Center=Centro aggiornamenti +Done=Fatto +Go\ back\ to\ update\ center=Torna al Centro aggiornamenti diff --git a/core/src/main/resources/hudson/PluginManager/check_it.properties b/core/src/main/resources/hudson/PluginManager/check_it.properties index e87f4933037b53ec724cecf46988d17d781d2f14..d7c212fd7f135fc27dc1e939e286f9406f4a4d26 100644 --- a/core/src/main/resources/hudson/PluginManager/check_it.properties +++ b/core/src/main/resources/hudson/PluginManager/check_it.properties @@ -21,3 +21,4 @@ # THE SOFTWARE. Check\ now=Controlla ora +lastUpdated=Informazioni sugli aggiornamenti recuperate {0} fa diff --git a/core/src/main/resources/hudson/PluginManager/index.jelly b/core/src/main/resources/hudson/PluginManager/index.jelly index eaf8dc222a2bfc36d893ef0ac9bf2c39d433060c..6044daff14c1741ae407bf50b1d3aac26a9b7afd 100644 --- a/core/src/main/resources/hudson/PluginManager/index.jelly +++ b/core/src/main/resources/hudson/PluginManager/index.jelly @@ -29,7 +29,7 @@ THE SOFTWARE.
- ${%Select}: ${%All}, ${%None}
+ ${%Select}: ${%All}, ${%None}
${%UpdatePageDescription}
${%UpdatePageLegend(rootURL+'/updateCenter/')} diff --git a/core/src/main/resources/hudson/PluginManager/index_it.properties b/core/src/main/resources/hudson/PluginManager/index_it.properties index a61bcf98201623361e88b16653e24aeea5aa289b..629b741e088f98f5d71fcc07469860729876142b 100644 --- a/core/src/main/resources/hudson/PluginManager/index_it.properties +++ b/core/src/main/resources/hudson/PluginManager/index_it.properties @@ -24,4 +24,4 @@ All=Tutto None=Niente Select=Seleziona UpdatePageDescription=Questa pagina elenca gli aggiornamenti dei plugin attualmente in uso. -UpdatePageLegend=Le righe disabilitate sono gi\u00E0 aggiornate e in attesa di riavvio. Le righe ombreggiate ma selezionabili sono in corso o fallite. +UpdatePageLegend=Le righe disabilitate sono relative a plugin già aggiornati e in attesa del riavvio di Jenkins. Le righe ombreggiate ma selezionabili sono relativi ad aggiornamenti in corso o non riusciti. diff --git a/core/src/main/resources/hudson/PluginManager/index_zh_CN.properties b/core/src/main/resources/hudson/PluginManager/index_zh_CN.properties deleted file mode 100644 index 76df8fcfdf4871fdd27e01b05d86965ba06e04e9..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/PluginManager/index_zh_CN.properties +++ /dev/null @@ -1,27 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -All=\u5168\u9009 -None=\u5168\u4E0D\u9009 -Select=\u9009\u62E9 -UpdatePageDescription=\u6B64\u9875\u9762\u663E\u793A\u60A8\u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6\u7684\u53EF\u7528\u66F4\u65B0 -UpdatePageLegend=\u4E0D\u53EF\u64CD\u4F5C\u7684\u884C\u662F\u5DF2\u7ECF\u5347\u7EA7\uFF0C\u7B49\u5F85\u91CD\u542F\u7684\u3002\u7070\u6697\u4F46\u53EF\u4EE5\u9009\u62E9\u7684\u884C\u662F\u6B63\u5728\u5347\u7EA7\u6216\u5347\u7EA7\u5931\u8D25\u7684\u3002 diff --git a/core/src/main/resources/hudson/PluginManager/installed_it.properties b/core/src/main/resources/hudson/PluginManager/installed_it.properties index 2be147af2a3a07f1079642d9cdaaa49f304b847a..a1361443e9b08becd079f771c3ad652f1aceefed 100644 --- a/core/src/main/resources/hudson/PluginManager/installed_it.properties +++ b/core/src/main/resources/hudson/PluginManager/installed_it.properties @@ -20,12 +20,25 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Changes\ will\ take\ effect\ when\ you\ restart\ Jenkins=Le modifiche avranno effetto quando riavvierai Jenkins +Changes\ will\ take\ effect\ when\ you\ restart\ Jenkins=Le modifiche avranno effetto al riavvio di Jenkins Enabled=Attivo Name=Nome Previously\ installed\ version=Versione precedente -Restart\ Once\ No\ Jobs\ Are\ Running=Riavvia quando non ci sono lavori in esecuzione -Uncheck\ to\ disable\ the\ plugin=Deseleziona per disattivare il plugin -Uninstall=Disintalla +Restart\ Once\ No\ Jobs\ Are\ Running=Riavvia quando non ci sono processi in esecuzione +Uncheck\ to\ disable\ the\ plugin=Deselezionare per disattivare il plugin +Uninstall=Disinstalla Version=Versione downgradeTo=Retrocedi a +It\ has\ one\ or\ more\ disabled\ dependencies=Ha una o pi dipendenze disabilitate +Update\ Center=Centro aggiornamenti +This\ plugin\ cannot\ be\ uninstalled=Impossibile disinstallare questo plugin +requires.restart=Quest''istanza di Jenkins deve essere riavviata. La modifica dello stato dei plugin in questa condizione caldamente sconsigliata. Riavviare Jenkins prima di procedere. +No\ plugins\ installed.=Nessun plugin installato. +Filter=Filtro +It\ has\ one\ or\ more\ installed\ dependants=Ha uno o pi plugin dipendenti installati +It\ has\ one\ or\ more\ enabled\ dependants=Ha uno o pi plugin dipendenti abilitati +This\ plugin\ cannot\ be\ enabled=Questo plugin non pu essere abilitato +This\ plugin\ cannot\ be\ disabled=Questo plugin non pu essere disabilitato +No\ description\ available.=Nessuna descrizione disponibile. +Uninstallation\ pending=Disinstallazione in sospeso +Warning=Avviso diff --git a/core/src/main/resources/hudson/PluginManager/installed_nl.properties b/core/src/main/resources/hudson/PluginManager/installed_nl.properties index aa7cc818944895680c9fafd0b01dbf47cf58fb2d..2133f18ddc80460d606648171a860e4ed079bc1d 100644 --- a/core/src/main/resources/hudson/PluginManager/installed_nl.properties +++ b/core/src/main/resources/hudson/PluginManager/installed_nl.properties @@ -21,10 +21,10 @@ # THE SOFTWARE. No\ plugins\ installed.=Er werd nog geen enkele plugin ge\u00EFnstalleerd. -New\ plugins\ will\ take\ effect\ once\ you\ restart\ Jenkins=Neuw geregistreerde plugins worden pas actie na het herstarten van Jenkins. +New\ plugins\ will\ take\ effect\ once\ you\ restart\ Jenkins=Nieuw geregistreerde plugins worden pas actief na het herstarten van Jenkins. Changes\ will\ take\ effect\ when\ you\ restart\ Jenkins=Uw wijzigingen zullen actief worden na het herstarten van Jenkins. Restart\ Once\ No\ Jobs\ Are\ Running=Opnieuw starten -Uncheck\ to\ disable\ the\ plugin=Vink aan om de plugin te de-activeren. +Uncheck\ to\ disable\ the\ plugin=Vink aan om de plugin te deactiveren. Enabled=Actief Name=Naam Version=Versie diff --git a/core/src/main/resources/hudson/PluginManager/installed_pl.properties b/core/src/main/resources/hudson/PluginManager/installed_pl.properties index 882151920ae8ab28e0c4d0d7fdd885a615f64b3d..4fb0ab51f38e684b709ed63b14ead78d5bce3bec 100644 --- a/core/src/main/resources/hudson/PluginManager/installed_pl.properties +++ b/core/src/main/resources/hudson/PluginManager/installed_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2016, Sun Microsystems, Damian Szczepanik +# Copyright (c) 2004-2017, Sun Microsystems, Damian Szczepanik # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,12 +28,14 @@ Restart\ Once\ No\ Jobs\ Are\ Running=Uruchom ponownie gdy \u017Cadne zadania ni Uncheck\ to\ disable\ the\ plugin=Odznacz aby wy\u0142\u0105czy\u0107 wtyczk\u0119 Uninstall=Odinstaluj Version=Wersja -downgradeTo=Powr\u00F3\u0107 do starszej wersji {0} +downgradeTo=Powr\u00F3\u0107 do wersji {0} requires.restart=Wymagane jest ponowne uruchomienie Jenkinsa. Zmiany wtyczek w tym momencie s\u0105 bardzo niewskazane. Uruchom ponownie Jenkinsa, zanim wprowadzisz zmiany. Uninstallation\ pending=Trwa odinstalowywanie This\ plugin\ cannot\ be\ disabled=Ta wtyczka nie mo\u017Ce by\u0107 wy\u0142\u0105czona No\ plugins\ installed.=Brak zainstalowanych wtyczek -This\ plugin\ cannot\ be\ enabled=Ta wtyczka nie mo\u017Ce by\u0107 wy\u0142\u0105czona +This\ plugin\ cannot\ be\ enabled=Ta wtyczka nie mo\u017Ce by\u0107 w\u0142\u0105czona Update\ Center=Centrum aktualizacji Filter=Filtruj No\ description\ available.=Opis nie jest dost\u0119pny +Warning=Ostrze\u017Cenie +New\ plugins\ will\ take\ effect\ once\ you\ restart\ Jenkins=Nowe wtyczki zostan\u0105 w\u0142\u0105czone po ponownym uruchomieniu Jenkinsa. diff --git a/core/src/main/resources/hudson/PluginManager/installed_zh_CN.properties b/core/src/main/resources/hudson/PluginManager/installed_zh_CN.properties deleted file mode 100644 index cef268874e540eb42c2222a5bcade4e6c00a3ef0..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/PluginManager/installed_zh_CN.properties +++ /dev/null @@ -1,31 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Changes\ will\ take\ effect\ when\ you\ restart\ Jenkins=\u6240\u6709\u6539\u53D8\u4F1A\u5728\u91CD\u65B0\u542F\u52A8Jenkins\u4EE5\u540E\u751F\u6548\u3002 -Enabled=\u542F\u7528 -Name=\u540D\u79F0 -Previously\ installed\ version=\u4E0A\u4E00\u4E2A\u5B89\u88C5\u7684\u7248\u672C -Restart\ Once\ No\ Jobs\ Are\ Running=\u5F53\u6CA1\u6709\u4EFB\u52A1\u65F6\u91CD\u542F -Uncheck\ to\ disable\ the\ plugin=\u53D6\u6D88\u9009\u62E9\u4EE5\u7981\u7528\u63D2\u4EF6 -Uninstall=\u5378\u8F7D -Version=\u7248\u672C -downgradeTo=\u964D\u5230 diff --git a/core/src/main/resources/hudson/PluginManager/sidepanel_it.properties b/core/src/main/resources/hudson/PluginManager/sidepanel_it.properties index e958a8920a6662354d75c26989c7ce2fdb343bb2..c96e127302423eb5a100c094ca6157131abb07c1 100644 --- a/core/src/main/resources/hudson/PluginManager/sidepanel_it.properties +++ b/core/src/main/resources/hudson/PluginManager/sidepanel_it.properties @@ -20,5 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Back\ to\ Dashboard=Torna alla Dashboard +Back\ to\ Dashboard=Torna al cruscotto Manage\ Jenkins=Configura Jenkins diff --git a/core/src/main/resources/hudson/PluginManager/sites_it.properties b/core/src/main/resources/hudson/PluginManager/sites_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..9dc12245d206aa0643f62647168207ac3a1c8601 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/sites_it.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# 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. + +Update\ Center=Centro aggiornamenti +Remove=Rimuovi +Add...=Aggiungi... diff --git a/core/src/main/resources/hudson/PluginManager/tabBar_it.properties b/core/src/main/resources/hudson/PluginManager/tabBar_it.properties index 96dce41f20e5c74795974e65a1e0f7834c40cddf..9cff73b66f0b91e5d7c271654e22f92c90b6b9cd 100644 --- a/core/src/main/resources/hudson/PluginManager/tabBar_it.properties +++ b/core/src/main/resources/hudson/PluginManager/tabBar_it.properties @@ -24,3 +24,4 @@ Advanced=Avanzate Available=Disponibili Installed=Installati Updates=Aggiornamenti +Sites=Siti diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index 52fed7654972f959acec2c9943fcd857db702e40..8052c1754c6fcb4e3a466f06ded3a1e93f3f99b3 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -72,6 +72,7 @@ THE SOFTWARE. + @@ -90,9 +91,10 @@ THE SOFTWARE. + disabled="${installedOk ? 'disabled' : null}" onClick="flip(this);" + data-compat-warning="${!p.isCompatibleWithInstalledVersion()}" /> - +
@@ -105,12 +107,18 @@ THE SOFTWARE.
${%coreWarning(p.requiredCore)}
- + +
${%javaWarning(p.minimumJavaVersion)}
+
+
${%depCompatWarning}
- +
${%depCoreWarning(p.getNeededDependenciesRequiredCore().toString())}
+ +
${%depJavaWarning(p.getNeededDependenciesMinimumJavaVersion().toString())}
+
${%securityWarning}
    diff --git a/core/src/main/resources/hudson/PluginManager/table.properties b/core/src/main/resources/hudson/PluginManager/table.properties index 63c161534cefcfc384899a32d6c4174d3094c1d3..08490598872d3e1cb494f076008d0119761731a2 100644 --- a/core/src/main/resources/hudson/PluginManager/table.properties +++ b/core/src/main/resources/hudson/PluginManager/table.properties @@ -26,6 +26,9 @@ compatWarning=\ coreWarning=\ Warning: This plugin is built for Jenkins {0} or newer. \ Jenkins will refuse to load this plugin if installed. +javaWarning=\ + Warning: This plugin requires Java {0} or newer. \ + Jenkins will refuse to load this plugin if installed. depCompatWarning=\ Warning: This plugin requires dependent plugins be upgraded and at least one of these dependent plugins claims to use a different settings format than the installed version. \ Jobs using that plugin may need to be reconfigured, and/or you may not be able to cleanly revert to the prior version without manually restoring old settings. \ @@ -34,6 +37,10 @@ depCoreWarning=\ Warning: This plugin requires dependent plugins that require Jenkins {0} or newer. \ Jenkins will refuse to load the dependent plugins requiring a newer version of Jenkins, \ and in turn loading this plugin will fail. +depJavaWarning=\ + Warning: this plugin requires other plugins which in turn require Java {0} or newer. \ + Jenkins will refuse to load the dependent plugins requiring a newer version of Jenkins, \ + and in turn loading this plugin will fail. securityWarning=\ Warning: This plugin version may not be safe to use. Please review the following security notices: diff --git a/core/src/main/resources/hudson/PluginManager/table_bg.properties b/core/src/main/resources/hudson/PluginManager/table_bg.properties index 893174efce4dcc7ef700a68b8bbee745d59fe5e2..25e4b2ed410bf82cd7a902cb762ea3ffd536ab3f 100644 --- a/core/src/main/resources/hudson/PluginManager/table_bg.properties +++ b/core/src/main/resources/hudson/PluginManager/table_bg.properties @@ -1,6 +1,6 @@ # The MIT License # -# Bulgarian translation: Copyright (c) 2015, 2016, Alexander Shopov +# Bulgarian translation: Copyright (c) 2015, 2016, 2017, Alexander Shopov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -61,3 +61,8 @@ depCompatWarning=\ \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435: \u0442\u0430\u0437\u0438 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430 \u0438\u0437\u0438\u0441\u043a\u0432\u0430 \u043e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0437\u0430\u0432\u0438\u0441\u0435\u0449\u0438\u0442\u0435 \u043e\u0442 \u043d\u0435\u044f\ \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0438. \u0427\u0430\u0441\u0442 \u043e\u0442 \u0442\u044f\u0445 \u043d\u0435 \u0441\u0430 \u0441\u044a\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u0438 \u0441 \u0442\u0435\u043a\u0443\u0449\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Jenkins. \u0429\u0435\ \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435 \u0437\u0430\u0434\u0430\u0447\u0438\u0442\u0435, \u043a\u043e\u0438\u0442\u043e \u0433\u0438 \u043f\u043e\u043b\u0437\u0432\u0430\u0442. +# \ +# Warning: This plugin version may not be safe to use. Please review the following security notices: +securityWarning=\ + \u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435: \u0442\u0430\u0437\u0438 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430\u0442\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0438\u043c\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0441\u044a\u0441\ + \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442\u0442\u0430. \u041f\u0440\u0435\u0433\u043b\u0435\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f\u0442\u0430. diff --git a/core/src/main/resources/hudson/PluginManager/table_it.properties b/core/src/main/resources/hudson/PluginManager/table_it.properties index f33abc80bbf7f5f5739e220334c3e0168dc3e9b9..2273eb9dc9e90829318e44849a7165bdad10ba29 100644 --- a/core/src/main/resources/hudson/PluginManager/table_it.properties +++ b/core/src/main/resources/hudson/PluginManager/table_it.properties @@ -20,14 +20,31 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Check\ to\ install\ the\ plugin=Seleziona per installare l''estensione +Check\ to\ install\ the\ plugin=Seleziona per installare il plugin Click\ this\ heading\ to\ sort\ by\ category=Clicca questa intestazione per ordinare in base alla categoria -Download\ now\ and\ install\ after\ restart=Scarica ora ed installa dopo il riavvio +Download\ now\ and\ install\ after\ restart=Scarica ora e installa dopo il riavvio Filter=Filtro -Inactive=Non attiva +Inactive=Non attivo Install=Installa Install\ without\ restart=Installa senza riavviare Installed=Installato Name=Nome No\ updates=Nessun aggiornamento Version=Versione +Update\ Center=Centro aggiornamenti +depCoreWarning=\ + Attenzione: questo plugin richiede dei plugin dipendenti che richiedono Jenkins {0} o successivo. \ + Jenkins si rifiuter di caricare i plugin dipendenti che richiedono una versione pi recente di Jenkins, \ + e il caricamento di questo plugin non riuscir a sua volta. +compatWarning=\ + Attenzione: la nuova versione di questo plugin dichiara di utilizzare un formato delle impostazioni differente da quello utilizzato dalla versione installata. \ + possibile che si debbano riconfigurare i processi che utilizzano questo plugin, e/o non si potrebbe essere in grado di ripristinare integralmente la versione precedente senza ripristinare le vecchie impostazioni manualmente. \ + Consultare le note di rilascio del plugin per i dettagli. +securityWarning=\ + Attenzione: possibile che questa versione del plugin non sia sicura da utilizzare. Si esaminino i seguenti avvisi di sicurezza: +depCompatWarning=\ + Attenzione: questo plugin richiede che dei plugin dipendenti siano aggiornati e almeno uno di questi plugin dipendente dichiara di utilizzare un formato delle impostazioni differente da quello utilizzato dalla versione installata. \ + possibile che si debbano riconfigurare i processi che utilizzano questo plugin, e/o non si potrebbe essere in grado di ripristinare integralmente la versione precedente senza ripristinare le vecchie impostazioni manualmente. \ + Consultare le note di rilascio del plugin per i dettagli. +coreWarning=Attenzione: questo plugin progettato per Jenkins {0} o una versione successiva. \ + Jenkins non si caricher se questo plugin installato. diff --git a/core/src/main/resources/hudson/PluginManager/table_zh_CN.properties b/core/src/main/resources/hudson/PluginManager/table_zh_CN.properties deleted file mode 100644 index 54a4c2ca6fc0d08418daac2b2de4b2704c28c1f2..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/PluginManager/table_zh_CN.properties +++ /dev/null @@ -1,34 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Check\ to\ install\ the\ plugin=\u8BF7\u52FE\u9009\u8981\u5B89\u88C5\u7684\u63D2\u4EF6 -Click\ this\ heading\ to\ sort\ by\ category=\u70B9\u51FB\u6807\u9898\u6309\u5206\u7C7B\u6392\u5E8F -Download\ now\ and\ install\ after\ restart=\u4E0B\u8F7D\u5F85\u91CD\u542F\u540E\u5B89\u88C5 -Filter=\u8FC7\u6EE4 -Inactive=\u672A\u6FC0\u6D3B -Install=\u5B89\u88C5 -Install\ without\ restart=\u76F4\u63A5\u5B89\u88C5 -Installed=\u5DF2\u5B89\u88C5 -Name=\u540D\u79F0 -No\ updates=\u6CA1\u6709\u66F4\u65B0 -Version=\u7248\u672C -coreWarning=\u8B66\u544A\uFF1A\u8BE5\u63D2\u4EF6\u53EA\u9002\u7528\u4E8EJenkins{0}\u6216\u66F4\u65B0\u7248\u672C\u3002\u5B83\u53EF\u80FD\u4E0D\u80FD\u6B63\u5E38\u8FD0\u884C\u4E8E\u4F60\u7684Jenkins diff --git a/core/src/main/resources/hudson/PluginManager/table_zh_TW.properties b/core/src/main/resources/hudson/PluginManager/table_zh_TW.properties index 058f101ec150c70bf24b1cd82d39b771625fbe35..dfde632a79abfa80aa915ef15219ca4ac4442bf9 100644 --- a/core/src/main/resources/hudson/PluginManager/table_zh_TW.properties +++ b/core/src/main/resources/hudson/PluginManager/table_zh_TW.properties @@ -21,25 +21,24 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Update\ Center=\u66f4\u65b0\u4e2d\u5fc3 +Update\ Center=\u66F4\u65B0\u4E2D\u5FC3 -Filter=\u904e\u6ffe\u689d\u4ef6 - -Check\ to\ install\ the\ plugin=\u5c07\u60f3\u8981\u5b89\u88dd\u7684\u5916\u639b\u7a0b\u5f0f\u6838\u53d6\u8d77\u4f86 -Click\ this\ heading\ to\ sort\ by\ category=\u5728\u6a19\u984c\u4e0a\u6309\u4e00\u4e0b\u53ef\u4ee5\u4f9d\u985e\u5225\u9032\u884c\u6392\u5e8f -Install=\u5b89\u88dd -Name=\u540d\u7a31 -Version=\u7248\u672c -Installed=\u5df2\u5b89\u88dd +Filter=\u904E\u6FFE\u689D\u4EF6 +Check\ to\ install\ the\ plugin=\u5C07\u60F3\u8981\u5B89\u88DD\u7684\u5916\u639B\u7A0B\u5F0F\u6838\u53D6\u8D77\u4F86 +Click\ this\ heading\ to\ sort\ by\ category=\u5728\u6A19\u984C\u4E0A\u6309\u4E00\u4E0B\u53EF\u4EE5\u4F9D\u985E\u5225\u9032\u884C\u6392\u5E8F +Install=\u5B89\u88DD +Name=\u540D\u7A31 +Version=\u7248\u672C +Installed=\u5DF2\u5B89\u88DD compatWarning=\ - \u8b66\u544a: \u65b0\u7248\u672c\u4e0d\u76f8\u5bb9\u65bc\u73fe\u5728\u5b89\u88dd\u7684\u7248\u672c\u3002\ - \u4f7f\u7528\u9019\u500b\u5916\u639b\u7a0b\u5f0f\u7684\u4f5c\u696d\u53ef\u80fd\u9700\u8981\u91cd\u65b0\u8a2d\u5b9a\u3002 + \u8B66\u544A: \u65B0\u7248\u672C\u4E0D\u76F8\u5BB9\u65BC\u73FE\u5728\u5B89\u88DD\u7684\u7248\u672C\u3002\ + \u4F7F\u7528\u9019\u500B\u5916\u639B\u7A0B\u5F0F\u7684\u4F5C\u696D\u53EF\u80FD\u9700\u8981\u91CD\u65B0\u8A2D\u5B9A\u3002 coreWarning=\u8B66\u544A: \u9019\u500B\u5916\u639B\u7A0B\u5F0F\u662F\u70BA Jenkins {0} \u4E4B\u5F8C\u7684\u7248\u672C\u8A2D\u8A08\u3002\u5728\u60A8\u76EE\u524D\u7684 Jenkins \u4E0D\u898B\u5F97\u53EF\u4EE5\u6B63\u5E38\u904B\u4F5C\u3002 Inactive=\u505C\u7528 -No\ updates=\u6c92\u6709\u66f4\u65b0 +No\ updates=\u6C92\u6709\u66F4\u65B0 Install\ without\ restart=\u76F4\u63A5\u5B89\u88DD Download\ now\ and\ install\ after\ restart=\u4E0B\u8F09\u4E26\u65BC\u91CD\u65B0\u555F\u52D5\u5F8C\u5B89\u88DD diff --git a/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.jelly b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.jelly index bbffff4593374eb20815747d4b3b3f4333e9afb6..067f0c1c973bfd0fa9712950c7ddd78113d9334f 100644 --- a/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.jelly @@ -1,22 +1,34 @@ -
    -
    -
    - -
    -
    - ${%Dependency errors}: -
      - -
    • ${plugin.longName} v${plugin.version} -
        - -
      • ${d}
      • -
        -
      -
    • +
    -
    + + + + +
diff --git a/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.properties b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.properties index cbad3bfe78e220dad70d3fbac95042d1b14d4e7b..9313dac39c7199e67b9bb587246e18fc0d464cf1 100644 --- a/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.properties +++ b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message.properties @@ -19,4 +19,5 @@ # 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. -Dependency\ errors=There are dependency errors loading some plugins +blurbOriginal=Some plugins could not be loaded due to unsatisfied dependencies. Fix these issues and restart Jenkins to restore the functionality provided by these plugins. +blurbDerived=These plugins failed to load because of one or more of the errors above. Fix those and these plugins will load again. diff --git a/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message_bg.properties b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..4c826414ea600f0d99a016cd39200c3e7fb642ca --- /dev/null +++ b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message_bg.properties @@ -0,0 +1,27 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, 2017, Alexander Shopov +# +# 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. + +Correct=\ + \u041e\u0442\u0433\u043e\u0432\u0430\u0440\u044f +# There are dependency errors loading some plugins +Dependency\ errors=\ + \u0418\u043c\u0430 \u0433\u0440\u0435\u0448\u043a\u0438 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438\u0442\u0435 \u043d\u0430 \u043d\u044f\u043a\u043e\u0438 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0438 diff --git a/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message_it.properties b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..96a114d9bdc457075824abb2d158f82293bc95dc --- /dev/null +++ b/core/src/main/resources/hudson/PluginWrapper/PluginWrapperAdministrativeMonitor/message_it.properties @@ -0,0 +1,24 @@ +# The MIT License +# +# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# 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. + +Dependency\ errors=Si sono verificati degli errori relativi alle dipendenze durante il caricamento di alcuni plugin +Correct=Correggi diff --git a/core/src/main/resources/hudson/PluginWrapper/thirdPartyLicenses_it.properties b/core/src/main/resources/hudson/PluginWrapper/thirdPartyLicenses_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..fd6be0e86d81a1c410ef72baa70613bc25747bdd --- /dev/null +++ b/core/src/main/resources/hudson/PluginWrapper/thirdPartyLicenses_it.properties @@ -0,0 +1,25 @@ +# The MIT License +# +# Copyright (c) 2004-, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of other of contributors +# +# 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. + +3rd\ Party\ Dependencies=Dipendenze di terze parti +about=Informazioni su {0} +No\ information\ recorded=Nessuna informazione registrata diff --git a/core/src/main/resources/hudson/PluginWrapper/uninstall_it.properties b/core/src/main/resources/hudson/PluginWrapper/uninstall_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..c4350041b6b5d41a657bdd060ea269703da10b79 --- /dev/null +++ b/core/src/main/resources/hudson/PluginWrapper/uninstall_it.properties @@ -0,0 +1,3 @@ +msg=Si sta per disinstallare il plugin {0}. Ciò rimuoverà il binario del plugin dalla propria directory $JENKINS_HOME, \ + ma non altererà i file di configurazione del plugin. +title=Disinstallazione del plugin {0} diff --git a/core/src/main/resources/hudson/ProxyConfiguration/config_it.properties b/core/src/main/resources/hudson/ProxyConfiguration/config_it.properties index 91419ea77d2c07167df2a2675e1a111935e2d731..0e8fa5ba491a1aafa877df63f5d33ec55274822c 100644 --- a/core/src/main/resources/hudson/ProxyConfiguration/config_it.properties +++ b/core/src/main/resources/hudson/ProxyConfiguration/config_it.properties @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Check\ now=Controlla ora -HTTP\ Proxy\ Configuration=Configurazione Proxy HTTP +Server=Server Port=Porta -Submit=Invia User\ name=Nome utente -lastUpdated=Informazioni di aggiornamento ottenute: {0} fa -uploadtext=Puoi fare l''upload di un file .hpi per installare +Password=Password +No\ Proxy\ Host=Eccezioni proxy +Test\ URL=URL di test +Validate\ Proxy=Controlla configurazione proxy diff --git a/core/src/main/resources/hudson/ProxyConfiguration/config_pl.properties b/core/src/main/resources/hudson/ProxyConfiguration/config_pl.properties index a03b5b7cb59974bed45b661ddb75f222aee4681b..fd2644805068d20c71308448f547000c5b1941ee 100644 --- a/core/src/main/resources/hudson/ProxyConfiguration/config_pl.properties +++ b/core/src/main/resources/hudson/ProxyConfiguration/config_pl.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc. +# Copyright (c) 2004-2017, Sun Microsystems, Inc, Damian Szczepanik # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -23,4 +23,6 @@ Password=Has\u0142o Port=Port Server=Serwer -User\ name=Nazwa u\u017cytkownika +User\ name=Nazwa u\u017Cytkownika +No\ Proxy\ Host=Brak hosta proxy +Validate\ Proxy=Sprawd\u017A proxy diff --git a/core/src/main/resources/hudson/ProxyConfiguration/config_zh_CN.properties b/core/src/main/resources/hudson/ProxyConfiguration/config_zh_CN.properties deleted file mode 100644 index afffdf88b25353e03b2e82ec8c85314b44822480..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/ProxyConfiguration/config_zh_CN.properties +++ /dev/null @@ -1,36 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Check\ now=\u7ACB\u5373\u83B7\u53D6 -File=\u6587\u4EF6 -HTTP\ Proxy\ Configuration=\u4EE3\u7406\u8BBE\u7F6E -Password=\u5BC6\u7801 -Port=\u7AEF\u53E3 -Server=\u670D\u52A1\u5668 -Submit=\u63D0\u4EA4 -URL=URL -Update\ Site=\u5347\u7EA7\u7AD9\u70B9 -Upload=\u4E0A\u4F20 -Upload\ Plugin=\u4E0A\u4F20\u63D2\u4EF6 -User\ name=\u7528\u6237\u540D -lastUpdated=\u66F4\u65B0\u4FE1\u606F\u83B7\u53D6\u4E8E{0}\u524D -uploadtext=\u60A8\u53EF\u4EE5\u901A\u8FC7\u4E0A\u4F20\u4E00\u4E2A.hpi\u6587\u4EF6\u6765\u5B89\u88C5\u63D2\u4EF6\u3002 diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-name_bg.html b/core/src/main/resources/hudson/ProxyConfiguration/help-name_bg.html new file mode 100644 index 0000000000000000000000000000000000000000..8cc660d9cbed6028958b113b9007882edc96dc81 --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-name_bg.html @@ -0,0 +1,13 @@ +
+ Ако вашият Jenkins е зад защитна стена и няма пряка връзка към Интернет, а JVM на сървъра не е + настроена правилно, за да се позволи връзка с Интернет, може да укажете име на сървър-посредник + за HTTP, за да позволите на Jenkins самостоятелно да инсталира приставки. (за повече детайли: + Networking Properties) + Jenkins използва HTTPS, за да се свърже със сървъра за обновяване и изтегли приставките. + +

+ Ако полето е празно, Jenkins ще се свързва директно с Интернет. + +

+ Ако не сте сигурни, вижте какви са настройките на браузъра ви. +

diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-name_it.html b/core/src/main/resources/hudson/ProxyConfiguration/help-name_it.html new file mode 100644 index 0000000000000000000000000000000000000000..463d4837671f519f42c5acd2c388c07b230c5a8d --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-name_it.html @@ -0,0 +1,13 @@ +
+ Se il proprio server Jenkins è protetto da un firewall e non ha accesso diretto a Internet, + e se la JVM del proprio server non è configurata in modo appropriato per abilitare la connessione a Internet + (si vedano le proprietà di rete JDK per ulteriori dettagli), + è possibile specificare in questo campo il nome del server proxy HTTP per consentire a Jenkins + di installare plugin per proprio conto. Si noti che Jenkins utilizza HTTPS per comunicare con il Centro aggiornamenti per scaricare i plugin. + +

+ Se si lascia questo campo vuoto, Jenkins proverà a connettersi direttamente a Internet. + +

+ Se non si è sicuri del valore da immettere, controllare la configurazione proxy del browser. +

diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_bg.html b/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_bg.html new file mode 100644 index 0000000000000000000000000000000000000000..03eeaa12cbe3f4640ef91c25a79066774c054112 --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_bg.html @@ -0,0 +1,4 @@ +
+ Шаблони за имената на сървърите, към които да се свързва директно, а не през сървъра-посредник. + По един шаблон на ред. „*“ напасва всички имена (напр. „*.cloudbees.com“ или „w*.jenkins.io“) +
diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_it.html b/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_it.html new file mode 100644 index 0000000000000000000000000000000000000000..41b4134959ca1571aad1784b99d7dea2c35dbc1e --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_it.html @@ -0,0 +1,4 @@ +
+ Specificare i pattern dei nomi host a cui non si dovrebbe accedere tramite il proxy (un host per riga). + "*" è il carattere jolly per i nomi host (come "*.jenkins.io" o "www*.jenkins-ci.org") +
diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_zh_CN.html b/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_zh_CN.html new file mode 100644 index 0000000000000000000000000000000000000000..54a7d1837a8f38190024655126a1a99aebb0df39 --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-noProxyHost_zh_CN.html @@ -0,0 +1,4 @@ +
+ 指定不通过代理的主机名格式,一个主机占一行。 + "*" 星号作为通配符 (例如 "*.jenkins.io" 或者 "www*.jenkins-ci.org") +
\ No newline at end of file diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-port_bg.html b/core/src/main/resources/hudson/ProxyConfiguration/help-port_bg.html new file mode 100644 index 0000000000000000000000000000000000000000..d24f470bdae430e3163cdf8d9c0befef8751c71a --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-port_bg.html @@ -0,0 +1,3 @@ +
+ Това поле определя порта за сървъра-посредник по HTTP. +
diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-port_it.html b/core/src/main/resources/hudson/ProxyConfiguration/help-port_it.html new file mode 100644 index 0000000000000000000000000000000000000000..90ca09ed13db6562e911de4d60c1bb101722c9fb --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-port_it.html @@ -0,0 +1,3 @@ +
+ Questo campo viene utilizzato insieme a quello del server proxy per specificare la porta del proxy HTTP. +
diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-port_zh_CN.html b/core/src/main/resources/hudson/ProxyConfiguration/help-port_zh_CN.html new file mode 100644 index 0000000000000000000000000000000000000000..906a82452e145064f6c90a8d5ff37ace3d787161 --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-port_zh_CN.html @@ -0,0 +1,3 @@ +
+ 该字段和代理服务器字段一起使用,用于指定HTTP代理端口。 +
\ No newline at end of file diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-userName_bg.html b/core/src/main/resources/hudson/ProxyConfiguration/help-userName_bg.html new file mode 100644 index 0000000000000000000000000000000000000000..d253066618dff8171ef7ad9c2478a5ac302f56bf --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-userName_bg.html @@ -0,0 +1,9 @@ +
+ Това поле определя името за идентификация пред сървъра-посредник . + +

+ Ако този сървър-посредник ползва схемата да идентификация на + Microsoft: NTLM, + то името за домейна се добавя пред потребителското име с + разделител „\“. Например: „ACME\John Doo“". +

diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-userName_it.html b/core/src/main/resources/hudson/ProxyConfiguration/help-userName_it.html new file mode 100644 index 0000000000000000000000000000000000000000..b7e82675b0a1f8f5e53ec4797691c33c273f4fff --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-userName_it.html @@ -0,0 +1,11 @@ +
+ Questo campo viene utilizzato insieme a quello del server proxy + per specificare il nome utente da utilizzare per l'autenticazione al proxy. + +

+ Se questo proxy richiede lo schema di autenticazione Microsoft + NTLM, è possibile codificare + il nome di dominio nel nome utente premettendo il nome di dominio e facendolo + seguire da una barra invertita '\' prima del nome utente, ad esempio + "ACME\Mario Rossi". +

diff --git a/core/src/main/resources/hudson/ProxyConfiguration/help-userName_zh_CN.html b/core/src/main/resources/hudson/ProxyConfiguration/help-userName_zh_CN.html new file mode 100644 index 0000000000000000000000000000000000000000..0d85ab4a39cbb131ff3fc57f6d76b34869a09860 --- /dev/null +++ b/core/src/main/resources/hudson/ProxyConfiguration/help-userName_zh_CN.html @@ -0,0 +1,7 @@ +
+ 该字段和代理服务器字段一起使用,用于指定代理的认证的用户名。 + +

+ 如果代理需要通过微软的 NTLM + 认证配置,那么域名和反斜杆'\'应该加到用户名前面,例如:"ACME\John Doo"。 +

\ No newline at end of file diff --git a/core/src/main/resources/hudson/cli/CLIAction/index.properties b/core/src/main/resources/hudson/cli/CLIAction/index.properties index e4eebf4895f7df8a40581f8f41ff9fdf39f88903..4808085a7a38638e949881ce3bbb82b8431c003c 100644 --- a/core/src/main/resources/hudson/cli/CLIAction/index.properties +++ b/core/src/main/resources/hudson/cli/CLIAction/index.properties @@ -1,4 +1,4 @@ Jenkins\ CLI=Jenkins CLI blurb=You can access various features in Jenkins through a command-line tool. See \ - the documentation for more details of this feature.\ + the documentation for more details of this feature. \ To get started, download jenkins-cli.jar, and run it as follows: diff --git a/core/src/main/resources/hudson/cli/CLIAction/index_bg.properties b/core/src/main/resources/hudson/cli/CLIAction/index_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..f1abd03aaa140c73a3d8f7fec9bc21ccab5fb5d8 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CLIAction/index_bg.properties @@ -0,0 +1,36 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, 2017 Alexander Shopov +# +# 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. + +Available\ Commands=\ + \u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u043a\u043e\u043c\u0430\u043d\u0434\u0438 +# You can access various features in Jenkins through a command-line tool. See \ +# the Wiki for more details of this feature.\ +# To get started, download jenkins-cli.jar, and run it as follows: +blurb=\ + \u0418\u043c\u0430\u0442\u0435 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 Jenkins \u0447\u0440\u0435\u0437 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430 \u0437\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u0438\u044f \u0440\u0435\u0434.\ + \u0417\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043f\u0440\u0435\u0433\u043b\u0435\u0434\u0430\u0439\u0442\u0435\ + \u0443\u0438\u043a\u0438\u0442\u043e.\ + \u0421\u0432\u0430\u043b\u0435\u0442\u0435 \u0444\u0430\u0439\u043b\u0430 jenkins-cli.jar \u0438 \u0433\u043e\ + \u0438\u0437\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u043f\u043e \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u043d\u0430\u0447\u0438\u043d: +# Jenkins CLI +Jenkins\ CLI=\ + Jenkins \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u0438\u044f \u0440\u0435\u0434 diff --git a/core/src/main/resources/hudson/cli/CLIAction/index_it.properties b/core/src/main/resources/hudson/cli/CLIAction/index_it.properties index 61db53365e174fc8e139a82cb822835a591d8afc..7d22486eeff508f3bd5a509f67bd8823c4791567 100644 --- a/core/src/main/resources/hudson/cli/CLIAction/index_it.properties +++ b/core/src/main/resources/hudson/cli/CLIAction/index_it.properties @@ -1,5 +1,5 @@ +Jenkins\ CLI=Interfaccia a riga di comando di Jenkins +blurb= possibile accedere a varie funzionalit di Jenkins tramite uno strumento a riga di comando. Si veda \ + la documentazione per ulteriori dettagli su questa funzionalit.\ + Per iniziare, scaricare jenkins-cli.jar ed eseguirlo come segue: Available\ Commands=Comandi disponibili -Jenkins\ CLI=Jenkins CLI -blurb=Puoi accede alle funzionalit\u00e0\u00a0 di Jenkins attraverso un tool da linea di comando. Per maggiori dettagli visita \ - la Wiki. \ - Per iniziare, scarica jenkins-cli.jar, e lancia il seguente comando: diff --git a/core/src/main/resources/hudson/cli/CLIAction/index_zh_CN.properties b/core/src/main/resources/hudson/cli/CLIAction/index_zh_CN.properties deleted file mode 100644 index bd6ca6d779ec5cf93bfab86cf1639fa22112c033..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/cli/CLIAction/index_zh_CN.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Available\ Commands=\u53EF\u7528\u7684\u547D\u4EE4 -Jenkins\ CLI=Jenkins \u547d\u4ee4\u884c -blurb=\u4F60\u53EF\u4EE5\u901A\u8FC7\u547D\u4EE4\u884C\u5DE5\u5177\u64CD\u4F5CJenkins\u7684\u8BB8\u591A\u7279\u6027\u3002\u4F60\u53EF\u4EE5\u901A\u8FC7 Wiki\u83B7\u5F97\u66F4\u591A\u4FE1\u606F\u3002\u4F5C\u4E3A\u5F00\u59CB\uFF0C\u4F60\u53EF\u4EE5\u4E0B\u8F7Djenkins-cli.jar\uFF0C\u7136\u540E\u8FD0\u884C\u4E0B\u5217\u547D\u4EE4\uFF1A diff --git a/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause.jelly b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause.jelly new file mode 100644 index 0000000000000000000000000000000000000000..9704ac6557baef7e5621b790be9b34112a827ed7 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause.jelly @@ -0,0 +1,4 @@ + + + ${%message} + diff --git a/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause.properties b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause.properties new file mode 100644 index 0000000000000000000000000000000000000000..c46f5db25da9e86b3288605e2f98f443a5ac9155 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause.properties @@ -0,0 +1,2 @@ +message=This protocol is an obsolete protocol, which has been replaced by CLI2-connect. \ + It is also not encrypted. diff --git a/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause_bg.properties b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..f6b2646c8a9a92f636c36abc7c0926834bd10ad8 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause_bg.properties @@ -0,0 +1,26 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2017, Alexander Shopov +# +# 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. + +# This protocol is an obsolete protocol, which has been replaced by CLI2-connect. \ +# It is also not encrypted. +message=\ + \u0422\u043e\u0437\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0435 \u043e\u0441\u0442\u0430\u0440\u044f\u043b \u0438 \u0435 \u0431\u0435\u0437 \u0448\u0438\u0444\u0440\u0438\u0440\u0430\u043d\u0435. \u0417\u0430\u043c\u0435\u043d\u0435\u043d \u0435 \u043e\u0442 CLI2-connect. diff --git a/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause_it.properties b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..54598b754e95246899d0099bdb438d344fd7558d --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol/deprecationCause_it.properties @@ -0,0 +1,2 @@ +message=Questo protocollo un protocollo obsoleto sostituito da CLI2-connect. \ + anche non criptato. diff --git a/core/src/main/resources/hudson/cli/CliProtocol/description_bg.properties b/core/src/main/resources/hudson/cli/CliProtocol/description_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..95e9c0e6e1be8b7c835ae2e4afa780091d525f77 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol/description_bg.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, 2017, Alexander Shopov +# +# 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. + +Accepts\ connections\ from\ CLI\ clients=\ + \u041f\u0440\u0438\u0435\u043c\u0430\u043d\u0435 \u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0438 \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0438\u0442\u0435 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u0438\u044f \u0440\u0435\u0434. +# Accepts connections from CLI clients. This protocol is unencrypted. +summary=\ + \u041f\u0440\u0438\u0435\u043c\u0430\u043d\u0435 \u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0438 \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0438\u0442\u0435 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u0438\u044f \u0440\u0435\u0434. \u0422\u043e\u0437\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u043d\u0435 \u0435 \u0437\u0430\u0449\u0438\u0442\u0435\u043d\ + \u0441 \u0448\u0438\u0444\u0440\u0438\u0440\u0430\u043d\u0435. diff --git a/core/src/main/resources/hudson/cli/CliProtocol/description_it.properties b/core/src/main/resources/hudson/cli/CliProtocol/description_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..5c876ca53a61ca699de2b777225af5cecc8b223b --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol/description_it.properties @@ -0,0 +1 @@ +summary=Accetta connessioni da client con interfaccia da riga di comando. Questo protocollo non criptato. diff --git a/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause.jelly b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause.jelly new file mode 100644 index 0000000000000000000000000000000000000000..9704ac6557baef7e5621b790be9b34112a827ed7 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause.jelly @@ -0,0 +1,4 @@ + + + ${%message} + diff --git a/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause.properties b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause.properties new file mode 100644 index 0000000000000000000000000000000000000000..8effec140412e3b6e8df2f0c0f3a10abde3e6612 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause.properties @@ -0,0 +1,3 @@ +message=Remoting-based CLI is deprecated and not recommended due to the security reasons. \ + It is recommended to disable this protocol on the instance. \ + if you need Remoting CLI on your instance, this protocol has to be enabled. diff --git a/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause_bg.properties b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..976f357b3d97283271607164f67e8c429852c92f --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause_bg.properties @@ -0,0 +1,29 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2017, Alexander Shopov +# +# 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. + +# Remoting-based CLI is deprecated and not recommended due to the security reasons. \ +# It is recommended to disable this protocol on the instance. \ +# if you need Remoting CLI on your instance, this protocol has to be enabled. +message=\ + \u041e\u0442\u0434\u0430\u043b\u0435\u0447\u0435\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0438 \u043e\u0442 \u043a\u043e\u043c\u0430\u043d\u0434\u0435\u043d \u0440\u0435\u0434 \u0441\u0430 \u043e\u0441\u0442\u0430\u0440\u0435\u043b\u0438 \u0438 \u043d\u0435 \u0441\u0435 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u0442 \u0437\u0430\u0440\u0430\u0434\u0438\ + \u043d\u0438\u0441\u043a\u0430\u0442\u0430 \u0438\u043c \u0441\u0438\u0433\u0443\u0440\u043d\u043e\u0441\u0442. \u041e\u0441\u0432\u0435\u043d \u0430\u043a\u043e \u043d\u0430\u0438\u0441\u0442\u0438\u043d\u0430 \u0441\u0435 \u043d\u0443\u0436\u0434\u0430\u0435\u0442\u0435 \u043e\u0442 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e \u0442\u043e\u0437\u0438\ + \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b, \u0432\u0438 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0434\u0430 \u0433\u043e \u0438\u0437\u043a\u043b\u044e\u0447\u0438\u0442\u0435. diff --git a/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause_it.properties b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..c85eea2078adf49430f2f32cb335393be65f0c55 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol2/deprecationCause_it.properties @@ -0,0 +1,3 @@ +message=L''interfaccia da riga di comando basata su remoting deprecata e non raccomandata per motivi di sicurezza. \ + raccomandato disabilitare questo protocollo sull''istanza. \ +Se richiesto l''utilizzo dell''interfaccia da riga di comando Remoting, questo protocollo deve essere abilitato. diff --git a/core/src/main/resources/hudson/cli/CliProtocol2/description_bg.properties b/core/src/main/resources/hudson/cli/CliProtocol2/description_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..d738341fc2f42366e1fa87071958ac21ef41cbf5 --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol2/description_bg.properties @@ -0,0 +1,27 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, 2017, Alexander Shopov +# +# 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. + +Extends\ the\ version\ 1\ protocol\ by\ adding\ transport\ encryption=\ + \u0420\u0430\u0437\u0448\u0438\u0440\u044f\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f 1 \u043d\u0430 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 \u043a\u0430\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u044f \u0448\u0438\u0444\u0440\u0438\u0440\u0430\u043d\u0435 +# Extends the version 1 protocol by adding transport encryption. +summary=\ + \u0420\u0430\u0437\u0448\u0438\u0440\u044f\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f 1 \u043d\u0430 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 \u043a\u0430\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u044f \u0448\u0438\u0444\u0440\u0438\u0440\u0430\u043d\u0435 diff --git a/core/src/main/resources/hudson/cli/CliProtocol2/description_it.properties b/core/src/main/resources/hudson/cli/CliProtocol2/description_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..cf122422da6ab80c7dafcad0119de333ec17d0da --- /dev/null +++ b/core/src/main/resources/hudson/cli/CliProtocol2/description_it.properties @@ -0,0 +1 @@ +summary=Estende la versione 1 del protocollo aggiungendo la crittografia del trasporto. diff --git a/core/src/main/resources/hudson/cli/Messages.properties b/core/src/main/resources/hudson/cli/Messages.properties index 8b9c933e6dc90092f473b1a0cfbf71842e02acba..85bbf56b6dad9e9ffa74e5b2ec88d778170bfb6b 100644 --- a/core/src/main/resources/hudson/cli/Messages.properties +++ b/core/src/main/resources/hudson/cli/Messages.properties @@ -7,6 +7,9 @@ InstallPluginCommand.NoUpdateCenterDefined=Note that no update center is defined InstallPluginCommand.NoUpdateDataRetrieved=No update center data is retrieved yet from: {0} InstallPluginCommand.NotAValidSourceName={0} is neither a valid file, URL, nor a plugin artifact name in the update center +EnablePluginCommand.NoSuchPlugin=No such plugin found with the name {0} +EnablePluginCommand.MissingDependencies=Cannot enable plugin {0} as it is missing the dependency {1} + AddJobToViewCommand.ShortDescription=\ Adds jobs to view. BuildCommand.ShortDescription=\ @@ -25,6 +28,8 @@ DeleteViewCommand.ShortDescription=\ Deletes view(s). DeleteJobCommand.ShortDescription=\ Deletes job(s). +EnablePluginCommand.ShortDescription=\ + Enables one or more installed plugins transitively. GroovyCommand.ShortDescription=\ Executes the specified Groovy script. GroovyshCommand.ShortDescription=\ @@ -108,3 +113,10 @@ WaitNodeOfflineCommand.ShortDescription=Wait for a node to become offline. CliProtocol.displayName=Jenkins CLI Protocol/1 (unencrypted) CliProtocol2.displayName=Jenkins CLI Protocol/2 (transport encryption) + +DisablePluginCommand.ShortDescription=\ +Disable one or more installed plugins. +DisablePluginCommand.NoSuchStrategy=This strategy ({0}) does not exist. Allowed strategies are {1} +DisablePluginCommand.PrintUsageSummary=\ +Disable the plugins with the given short names. You can define how to proceed with the dependant plugins and if a restart after should be done. You can also set the quiet mode to avoid extra info in the console. +DisablePluginCommand.StatusMessage=Disabling ''{0}'': {1} ({2}) diff --git a/core/src/main/resources/hudson/cli/Messages_bg.properties b/core/src/main/resources/hudson/cli/Messages_bg.properties index 3ce9a54d03d52307a3b3b8993c3b0965e43dabf7..09062a55444c30da0e791c8535caa71fb9121ecd 100644 --- a/core/src/main/resources/hudson/cli/Messages_bg.properties +++ b/core/src/main/resources/hudson/cli/Messages_bg.properties @@ -1,6 +1,6 @@ # The MIT License # -# Bulgarian translation: Copyright (c) 2015, 2016, Alexander Shopov +# Bulgarian translation: Copyright (c) 2015, 2016, 2017, Alexander Shopov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -159,3 +159,29 @@ CliProtocol2.displayName=\ # Jenkins CLI Protocol/1 CliProtocol.displayName=\ \u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u043d\u0430 Jenkins \u0437\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0435\u043d \u0440\u0435\u0434, \u0432\u0435\u0440\u0441\u0438\u044f 1 +# \ +# Depending on the security realm, you will need to pass something like:\n\ +# --username USER [ --password PASS | --password-file FILE ]\n\ +# May not be supported in some security realms, such as single-sign-on.\n\ +# Pair with the logout command.\n\ +# The same options can be used on any other command, but unlike other generic CLI options,\n\ +# these come *after* the command name.\n\ +# Whether stored or not, this authentication mode will not let you refer to (e.g.) jobs as arguments\n\ +# if Jenkins denies anonymous users Overall/Read and (e.g.) Job/Read.\n\ +# *Deprecated* in favor of -auth. +LoginCommand.FullDescription=\ + \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442 \u043e\u0442 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0449\u0435 \u0441\u0435 \u043d\u0430\u043b\u043e\u0436\u0438 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u043d\u0435\u0449\u043e \u043a\u0430\u0442\u043e:\n\ + --username \u041f\u041e\u0422\u0420\u0415\u0411\u0418\u0422\u0415\u041b [ --password \u041f\u0410\u0420\u041e\u041b\u0410 | --password-file \u0424\u0410\u0419\u041b ]\n\ + \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0438 \u0432\u044a\u0432 \u0432\u0441\u0438\u0447\u043a\u0438 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438, \u043d\u0430\u043f\u0440. \u043f\u0440\u0438 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u043e \u0432\u043f\u0438\u0441\u0432\u0430\u043d\u0435.\n\ + \u041a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u0430\u0439\u0442\u0435 \u0441 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0442\u0430 \u0437\u0430 \u0438\u0437\u0445\u043e\u0434.\n\ + \u0422\u0435\u0437\u0438 \u043e\u043f\u0446\u0438\u0438 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 \u0441 \u0432\u0441\u044f\u043a\u0430 \u0434\u0440\u0443\u0433\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430, \u043d\u043e \u0437\u0430 \u0440\u0430\u0437\u043b\u0438\u043a\u0430 \u043e\u0442\ + \u043e\u0442 \u043e\u0441\u0442\u0430\u043d\u0430\u043b\u0438\u0442\u0435 \u043e\u0431\u0449\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0435\u043d \u0440\u0435\u0434, \u0442\u0435 \u0441\u0435 \u043f\u043e\u0434\u0430\u0432\u0430\u0442 \u0421\u041b\u0415\u0414 \u0438\u043c\u0435\u0442\u043e \u043d\u0430\ + \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0442\u0430. \u0422\u043e\u0437\u0438 \u0440\u0435\u0436\u0438\u043c \u043d\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0432\u0430 \u0434\u0430 \u0443\u043a\u0430\u0437\u0432\u0430\u0442\u0435 \u0438\u0437\u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0438\u044f\u0442\u0430 \u043a\u0430\u0442\u043e \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438,\ + \u0430\u043a\u043e Jenkins \u043d\u0435 \u0434\u0430\u0432\u0430 \u043d\u0430 \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u0438\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438 \u043e\u0431\u0449\u043e \u043f\u0440\u0430\u0432\u043e \u0437\u0430 \u0447\u0435\u0442\u0435\u043d\u0435.\ + \u041e\u0421\u0422\u0410\u0420\u042f\u041b\u041e, \u0434\u0430 \u043d\u0435 \u0441\u0435 \u043f\u043e\u043b\u0437\u0432\u0430! \u041f\u0440\u043e\u0431\u0432\u0430\u0439\u0442\u0435 \u0441 \u201e-auth\u201c \u0432\u043c\u0435\u0441\u0442\u043e \u0442\u043e\u0432\u0430. +# Installing a plugin from standard input +InstallPluginCommand.InstallingPluginFromStdin=\ + \u0418\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0438\u044f \u0432\u0445\u043e\u0434. +# *Deprecated* in favor of -auth. +LogoutCommand.FullDescription=\ + \u041e\u0421\u0422\u0410\u0420\u042f\u041b\u041e, \u0434\u0430 \u043d\u0435 \u0441\u0435 \u043f\u043e\u043b\u0437\u0432\u0430! \u041f\u0440\u043e\u0431\u0432\u0430\u0439\u0442\u0435 \u0441 \u201e-auth\u201c \u0432\u043c\u0435\u0441\u0442\u043e \u0442\u043e\u0432\u0430. diff --git a/core/src/main/resources/hudson/cli/Messages_es.properties b/core/src/main/resources/hudson/cli/Messages_es.properties index 11525579ca6435b6433a4acfac31c4feecf6cd02..d8f7b4d7cc40eab48e10a877762da1843209f8d1 100644 --- a/core/src/main/resources/hudson/cli/Messages_es.properties +++ b/core/src/main/resources/hudson/cli/Messages_es.properties @@ -59,4 +59,9 @@ CancelQuietDownCommand.ShortDescription=Cancelar el efecto del comando "quiet-do OfflineNodeCommand.ShortDescription=Dejar de utilizar un nodo temporalmente hasta que se ejecute el comando "online-node". WaitNodeOnlineCommand.ShortDescription=Esperando hasta que el nodo est activado WaitNodeOfflineCommand.ShortDescription=Esperando a que el nodo est desactivado - +DisablePluginCommand.ShortDescription=\ +Deshabilita uno o ms plugins instalados. +DisablePluginCommand.NoSuchStrategy=Esta estrategia ({0}) no existe. Las estrategias permitidas son {1} +DisablePluginCommand.PrintUsageSummary=\ +Deshabilita los plugins con los nombre cortos dados. Puede definir cmo proceder con los plugins dependientes y si se realiza un reinicio posterior. +DisablePluginCommand.StatusMessage=Deshabilitando {0}: {1} ({2}) diff --git a/core/src/main/resources/hudson/cli/Messages_it.properties b/core/src/main/resources/hudson/cli/Messages_it.properties index 529ebd742f4247513289099d3b1519dec2a0319c..ab81259761056b8beb92704373a19d93ca16af5e 100644 --- a/core/src/main/resources/hudson/cli/Messages_it.properties +++ b/core/src/main/resources/hudson/cli/Messages_it.properties @@ -1,7 +1,78 @@ -DeleteNodeCommand.ShortDescription=Cancella un nodo -DeleteJobCommand.ShortDescription=Cancella un job -ClearQueueCommand.ShortDescription=Pulisce la coda di lavoro -ConnectNodeCommand.ShortDescription=Riconnettersi ad un nodo -WaitNodeOnlineCommand.ShortDescription=Attende che il nodo sia attivo -WaitNodeOfflineCommand.ShortDescription=Attende che un nodo sia disattivo +InstallPluginCommand.DidYouMean=Sembra che {0} sia il nome breve di un plugin. Forse si intendeva "{1}"? +InstallPluginCommand.InstallingFromUpdateCenter=Installazione di {0} dal Centro aggiornamenti in corso +InstallPluginCommand.InstallingPluginFromLocalFile=Installazione di un plugin da un file locale in corso: {0} +InstallPluginCommand.InstallingPluginFromStdin=Installazione di un plugin dallo standard input in corso +InstallPluginCommand.InstallingPluginFromUrl=Installazione di un plugin da {0} in corso +InstallPluginCommand.NoUpdateCenterDefined=Si noti che in quest''istanza di Jenkins non definito alcun Centro aggiornamenti. +InstallPluginCommand.NoUpdateDataRetrieved=Non sono ancora stati recuperati dati del Centro aggiornamenti da {0} +InstallPluginCommand.NotAValidSourceName={0} non n un file valido, n un URL, n il nome di un artefatto di un plugin nel Centro aggiornamenti +AddJobToViewCommand.ShortDescription=Aggiunge processi alla vista. +BuildCommand.ShortDescription=Compila una build e attende facoltativamente il suo completamento. +ConsoleCommand.ShortDescription=Recupera l''output su console di una build. +CopyJobCommand.ShortDescription=Copia un processo. +CreateJobCommand.ShortDescription=Crea un nuovo processo leggendo lo standard input e interpretandolo come un file di configurazione XML. +CreateNodeCommand.ShortDescription=Crea un nuovo nodo leggendo lo standard input e interpretandolo come un file di configurazione XML. +CreateViewCommand.ShortDescription=Crea una nuova vista leggendo lo standard input e interpretandolo come un file di configurazione XML. +DeleteBuildsCommand.ShortDescription=Elimina uno o pi record relativi alle build. +DeleteViewCommand.ShortDescription=Elimina una o pi viste. +DeleteJobCommand.ShortDescription=Elimina uno o pi processi. +GroovyCommand.ShortDescription=Esegue lo script Groovy specificato. +GroovyshCommand.ShortDescription=Esegue una shell Groovy interattiva. +HelpCommand.ShortDescription=Elenca tutti i comandi disponibili o visualizza una descrizione dettagliata di un singolo comando. +InstallPluginCommand.ShortDescription=Installa un plugin da un file, un URL o dal Centro aggiornamenti. +InstallToolCommand.ShortDescription=Esegue un''installazione automatica dello strumento e stampa il suo percorso sullo standard output. Pu essere invocato solo da una build. [deprecato] +ListChangesCommand.ShortDescription=Esegue il dump del log delle modifiche per la/le build specificata/e. +ListJobsCommand.ShortDescription=Elenca tutti i processi in una specifica vista o in un gruppo di elementi. +ListPluginsCommand.ShortDescription=Visualizza un elenco di plugin installati. +LoginCommand.ShortDescription=Salva le credenziali correnti per consentire ai comandi futuri di essere eseguiti senza specificare esplicitamente le informazioni relative alle credenziali. [deprecato] +LoginCommand.FullDescription=A seconda dell''area di sicurezza sar necessario fornire argomenti come:\n\ +--username UTENTE [ --password PASSWORD | --password-file FILE ]\n\ +Pu non essere supportato in alcune aree di sicurezza come il single sign on.\n\ +Va abbinato al comando logout.\n\ +Le medesime opzioni possono essere utilizzate con qualsiasi altro comando, ma,\n\ +a differenza delle altre opzioni generiche dell''interfaccia da riga di comando,\n\ +queste devono essere specificate *dopo* il nome del comando.\n\ +Indipendentemente dal fatto che le credenziali siano salvate o meno, questa\n\ +modalit di autenticazione non consentir di fare riferimento (ad esempio)\n\ +ai processi come argomenti se Jenkins nega agli utenti anonimi i permessi\n\ +Globale/Lettura e (ad esempio) Processo/Lettura.\n\ +*Deprecato* a favore di -auth. +LogoutCommand.ShortDescription=Elimina le credenziali salvate con il comando login. [deprecato] +LogoutCommand.FullDescription=*Deprecato* a favore di -auth. +MailCommand.ShortDescription=Legge lo standard input e lo invia come messaggio di posta elettronica. +SetBuildDescriptionCommand.ShortDescription=Imposta la descrizione di una build. +SetBuildParameterCommand.ShortDescription=Aggiorna/imposta il parametro di build della build attualmente in corso. [deprecato] +SetBuildResultCommand.ShortDescription=Imposta il risultato della build corrente. Funziona solamente se invocato da una build. [deprecato] +RemoveJobFromViewCommand.ShortDescription=Rimuove dei processi dalla vista. +VersionCommand.ShortDescription=Visualizza la versione attuale. +GetJobCommand.ShortDescription=Esegue il dump dell''XML della definizione del processo sullo standard output. +GetNodeCommand.ShortDescription=Esegue il dump dell''XML della definizione del nodo sullo standard output. +GetViewCommand.ShortDescription=Esegue il dump dell''XML della definizione della vista sullo standard output. +SetBuildDisplayNameCommand.ShortDescription=Imposta il nome visualizzato di una build. +WhoAmICommand.ShortDescription=Visualizza i propri credenziali e permessi. +UpdateJobCommand.ShortDescription=Aggiorna l''XML della definizione del processo dallo standard input. il comando contrario di get-job. +UpdateNodeCommand.ShortDescription=Aggiorna l''XML della definizione del nodo dallo standard input. il comando contrario di get-node. +SessionIdCommand.ShortDescription=Visualizza l''ID di sessione che cambia a ogni riavvio di Jenkins. +UpdateViewCommand.ShortDescription=Aggiorna l''XML della definizione della vista dallo standard input. il comando contrario di get-view. + +BuildCommand.CLICause.ShortDescription=Avviato dalla riga di comando da {0} +BuildCommand.CLICause.CannotBuildDisabled=Impossibile compilare {0} perch disabilitato. +BuildCommand.CLICause.CannotBuildConfigNotSaved=Impossibile compilare {0} perch la sua configurazione non stata salvata. +BuildCommand.CLICause.CannotBuildUnknownReasons=Impossibile compilare {0} per motivi sconosciuti. + +DeleteNodeCommand.ShortDescription=Elimina nodo/i +ReloadJobCommand.ShortDescription=Aggiorna processo/i +OnlineNodeCommand.ShortDescription=Riprende ad utilizzare un nodo per eseguire le build, annullando il comando precedente "offline-node". +ClearQueueCommand.ShortDescription=Pulisce la coda di lavoro. +ReloadConfigurationCommand.ShortDescription=Scarta tutti i dati caricati in memoria e ricarica tutto dal file system. Utile quando si sono modificati file di configurazione direttamente su disco. +ConnectNodeCommand.ShortDescription=Esegue una nuova connessione al nodo/i. +DisconnectNodeCommand.ShortDescription=Si disconnette da un nodo/i. +QuietDownCommand.ShortDescription=Ferma le attivit di Jenkins in preparazione a un riavvio. Non avvia alcuna build. +CancelQuietDownCommand.ShortDescription=Annulla gli effetti del comando "quiet-down". +OfflineNodeCommand.ShortDescription=Non utilizza pi temporaneamente un nodo per eseguire le build fino al successivo comando "online-node". +WaitNodeOnlineCommand.ShortDescription=Attende che un nodo sia in linea. +WaitNodeOfflineCommand.ShortDescription=Attende che un nodo sia non in linea. + +CliProtocol.displayName=Protocollo Jenkins da interfaccia da riga di comando/1 (non criptato) +CliProtocol2.displayName=Protocollo Jenkins da interfaccia da riga di comando/1 (trasporto criptato) diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/index_bg.properties b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/index_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..91faf92f5a063d6a10c4f1852e07e9572740eb9a --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/index_bg.properties @@ -0,0 +1,48 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, 2017, Alexander Shopov +# +# 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. + +# Clean up some files from this partition to make more room. +solution.1=\ + \u0418\u0437\u0442\u0440\u0438\u0439\u0442\u0435 \u0447\u0430\u0441\u0442 \u043e\u0442 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u043e\u0442 \u0442\u043e\u0437\u0438 \u0434\u044f\u043b, \u0437\u0430 \u0434\u0430 \u043e\u0441\u0432\u043e\u0431\u043e\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e. +# \ +# Move JENKINS_HOME to a bigger partition. \ +# See our Wiki for how to do this. +solution.2=\ + \u041f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u0442\u0435 JENKINS_HOME \u043d\u0430 \u043f\u043e-\u0433\u043e\u043b\u044f\u043c \u0434\u044f\u043b.\ + \u0412\u0438\u0436\u0442\u0435: \ + \u0443\u0438\u043a\u0438\u0442\u043e \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0442\u043e\u0432\u0430. +# JENKINS_HOME is almost full +blurb=\ + \u0424\u0430\u0439\u043b\u043e\u0432\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430, \u043d\u0430 \u043a\u043e\u044f\u0442\u043e \u0435 JENKINS_HOME, \u0435 \u043f\u043e\u0447\u0442\u0438 \u043f\u044a\u043b\u043d\u0430 +JENKINS_HOME\ is\ almost\ full=\ + \u0424\u0430\u0439\u043b\u043e\u0432\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430, \u043d\u0430 \u043a\u043e\u044f\u0442\u043e \u0435 JENKINS_HOME, \u0435 \u043f\u043e\u0447\u0442\u0438 \u043f\u044a\u043b\u043d\u0430 +# \ +# Your JENKINS_HOME ({0}) is almost full. \ +# When this directory completely fills up, it\'ll wreak havoc because Jenkins can\'t store any more data. +description.1=\ + \ + \u0424\u0430\u0439\u043b\u043e\u0432\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 {0}, \u043d\u0430 \u043a\u043e\u044f\u0442\u043e \u0435 JENKINS_HOME ({0}) \u0435 \u043f\u043e\u0447\u0442\u0438 \u043f\u044a\u043b\u043d\u0430.\ + \u041a\u043e\u0433\u0430\u0442\u043e \u0434\u0438\u0441\u043a\u043e\u0432\u043e\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0441\u0432\u044a\u0440\u0448\u0438, Jenkinns \u043d\u044f\u043c\u0430 \u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e, \u0437\u0430\u0449\u043e\u0442\u043e\ + \u043d\u044f\u043c\u0430 \u043a\u044a\u0434\u0435 \u0434\u0430 \u0441\u044a\u0445\u0440\u0430\u043d\u044f\u0432\u0430 \u043d\u043e\u0432\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438. +# To prevent that problem, you should act now. +description.2=\ + \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0440\u0435\u0430\u0433\u0438\u0440\u0430\u0442\u0435 \u043d\u0435\u0437\u0430\u0431\u0430\u0432\u043d\u043e, \u0437\u0430 \u0434\u0430 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0442\u0438\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c. diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/index_it.properties b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/index_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..791261199b8ace2475c5a449dc8162cf34f0e831 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/index_it.properties @@ -0,0 +1,10 @@ +blurb=JENKINS_HOME quasi piena +description.1=\ + JENKINS_HOME ({0}) quasi piena. \ + Quando questa directory sar completamente piena, ci causer scompiglio perch Jenkins non potr pi memorizzare dati. +description.2=Per prevenire tale problema, si dovrebbe agire ora. +solution.1=Eliminare alcuni file da questa partizione per creare pi spazio. +solution.2=\ + Spostare JENKINS_HOME su una partizione pi grande. \ + Si veda la nostra documentazione per scoprire come fare. +JENKINS_HOME\ is\ almost\ full=JENKINS_HOME quasi piena diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.jelly b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.jelly index beefff24ce7fae5534172e68c68a38b66dc26776..1ff3673ab660c441ac16daba0abb4423c755f5d6 100644 --- a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.jelly +++ b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.jelly @@ -24,13 +24,11 @@ THE SOFTWARE. -
-
-
- - -
- ${%blurb(app.rootDir)} -
-
-
\ No newline at end of file +
+
+ + + + ${%blurb(app.rootDir)} +
+ diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.properties b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.properties index 179c3ad6461b2aca6040f8894928ce47f954dee0..b8196734b1c0070fba41a269ea1730bccd136883 100644 --- a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.properties +++ b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message.properties @@ -1 +1 @@ -blurb=Your Jenkins data directory "{0}" (AKA JENKINS_HOME) is almost full. You should act on it before it gets completely full. \ No newline at end of file +blurb=Your Jenkins data directory {0} (AKA JENKINS_HOME) is almost full. You should act on it before it gets completely full. \ No newline at end of file diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_bg.properties b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..967bddc28aca5bd7ece915cd1b8c2eb2b2eecd56 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_bg.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, Alexander Shopov +# +# 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. + +Dismiss= +Tell\ me\ more=\ + \u041f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f +# Your Jenkins data directory "{0}" (AKA JENKINS_HOME) is almost full. You should act on it before it gets completely full. +blurb=\ + \u0414\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u0441\u0430 \u0434\u0430\u043d\u043d\u0438 \u043d\u0430 Jenkins \u201e{0}\u201c \u2014 JENKINS_HOME) diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_it.properties b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..a6a78adfd17904f7b1f75d7af9fba6c391fbdd7d --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_it.properties @@ -0,0 +1,3 @@ +blurb=La directory dati di Jenkins "{0}" (nota anche come JENKINS_HOME) quasi piena. Si dovrebbe agire prima che diventi completamente piena. +Tell\ me\ more=Dimmi di pi +Dismiss=Ignora diff --git a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_zh_CN.properties b/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_zh_CN.properties deleted file mode 100644 index 81263a6e4ebfac8b2074ef20712279c83313558c..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/diagnosis/HudsonHomeDiskUsageMonitor/message_zh_CN.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Dismiss=\u4E0D\u518D\u663E\u793A -Tell\ me\ more=\u66F4\u591A\u4FE1\u606F -blurb=\u4F60\u7684 Jenkins \u6570\u636E\u76EE\u5F55 "{0}" (AKA JENKINS_HOME) \u5C31\u5FEB\u8981\u7A7A\u95F4\u4E0D\u8DB3\u4E86\u3002\u4F60\u5E94\u8BE5\u5728\u5B83\u88AB\u5B8C\u5168\u6491\u6EE1\u4E4B\u524D\u5C31\u6709\u6240\u884C\u52A8\u3002 diff --git a/core/src/main/resources/hudson/scm/SCM/project-changes_zh_CN.properties b/core/src/main/resources/hudson/diagnosis/MemoryUsageMonitor/index_bg.properties similarity index 71% rename from core/src/main/resources/hudson/scm/SCM/project-changes_zh_CN.properties rename to core/src/main/resources/hudson/diagnosis/MemoryUsageMonitor/index_bg.properties index 7262342d01662f146c9e4bc0a40ea5c274b73d09..17fcc9d499abb46ab867c0e7bbfdc6060e38511c 100644 --- a/core/src/main/resources/hudson/scm/SCM/project-changes_zh_CN.properties +++ b/core/src/main/resources/hudson/diagnosis/MemoryUsageMonitor/index_bg.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc. +# Bulgarian translation: Copyright (c) 2016, Alexander Shopov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,5 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -No\ changes\ in\ any\ of\ the\ builds.=\u6CA1\u6709\u4EFB\u4F55\u53D8\u66F4\u3002 -detail=\u8BE6\u7EC6\u4FE1\u606F +Long=\ + \u0414\u044a\u043b\u044a\u0433 +Timespan=\ + \u041f\u0435\u0440\u0438\u043e\u0434 +Short=\ + \u041a\u0440\u0430\u0442\u044a\u043a +Medium=\ + \u0421\u0440\u0435\u0434\u0435\u043d +JVM\ Memory\ Usage=\ + \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u0430\u043c\u0435\u0442 \u043e\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u043d\u0430\u0442\u0430 \u043c\u0430\u0448\u0438\u043d\u0430 \u043d\u0430 Java diff --git a/core/src/main/resources/hudson/diagnosis/MemoryUsageMonitor/index_it.properties b/core/src/main/resources/hudson/diagnosis/MemoryUsageMonitor/index_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..e3dd2e29e2a08a5f6ad30d00a016fb25e97b015d --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/MemoryUsageMonitor/index_it.properties @@ -0,0 +1,5 @@ +JVM\ Memory\ Usage=Utilizzo memoria JVM +Timespan=Intervallo temporale +Short=Breve +Medium=Medio +Long=Lungo diff --git a/core/src/main/resources/hudson/diagnosis/Messages_bg.properties b/core/src/main/resources/hudson/diagnosis/Messages_bg.properties index 1a3ea6cfade1dc525ce73d04cc73991dbc9ed880..25432bc2ea3f1c0cefc31c10e46a3ed420361ac9 100644 --- a/core/src/main/resources/hudson/diagnosis/Messages_bg.properties +++ b/core/src/main/resources/hudson/diagnosis/Messages_bg.properties @@ -1,6 +1,6 @@ # The MIT License # -# Bulgarian translation: Copyright (c) 2015, 2016, Alexander Shopov +# Bulgarian translation: Copyright (c) 2015, 2016, 2017, Alexander Shopov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -31,3 +31,12 @@ OldDataMonitor.DisplayName=\ \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u0441\u0442\u0430\u0440\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 HudsonHomeDiskUsageMonitor.DisplayName=\ \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u043e \u0434\u0438\u0441\u043a\u043e\u0432\u043e \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e +# Reverse Proxy Setup +ReverseProxySetupMonitor.DisplayName=\ + \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430-\u043e\u0431\u0440\u0430\u0442\u0435\u043d \u043f\u043e\u0441\u0440\u0435\u0434\u043d\u0438\u043a +# Too Many Jobs Not Organized in Views +TooManyJobsButNoView.DisplayName=\ + \u041f\u0440\u0435\u043a\u0430\u043b\u0435\u043d\u043e \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u0434\u0430\u0447\u0438 \u043d\u0435 \u0441\u0430 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0438\u0440\u0430\u043d\u0438 \u043f\u043e \u0438\u0437\u0433\u043b\u0435\u0434\u0438 +# Missing Descriptor ID +NullIdDescriptorMonitor.DisplayName=\ + \u041b\u0438\u043f\u0441\u0432\u0430\u0449 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043d\u0430 \u0434\u0435\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0440 diff --git a/core/src/main/resources/hudson/diagnosis/Messages_it.properties b/core/src/main/resources/hudson/diagnosis/Messages_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..762f9fb1fda4430243b422930663f2f8fa5578e9 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/Messages_it.properties @@ -0,0 +1,9 @@ +MemoryUsageMonitor.USED=Utilizzata +MemoryUsageMonitor.TOTAL=Totale +OldDataMonitor.Description=Ripulisci i file di configurazione per rimuovere residui dovuti a vecchi plugin e vecchie versioni. +OldDataMonitor.DisplayName=Gestisci dati vecchi +HudsonHomeDiskUsageMonitor.DisplayName=Monitor utilizzo spazio su disco + +NullIdDescriptorMonitor.DisplayName=ID descrittore mancante +ReverseProxySetupMonitor.DisplayName=Impostazione reverse proxy +TooManyJobsButNoView.DisplayName=Troppi processi non organizzati in viste diff --git a/core/src/main/resources/hudson/diagnosis/Messages_ru.properties b/core/src/main/resources/hudson/diagnosis/Messages_ru.properties new file mode 100644 index 0000000000000000000000000000000000000000..3bddb6e1be12cf333395a3961bb110fec524c57f --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/Messages_ru.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright (c) 2017, Kseniia Nenasheva +# +# 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. + +MemoryUsageMonitor.TOTAL=\u0412\u0441\u0435\u0433\u043E +OldDataMonitor.Description=\u041E\u0447\u0438\u0441\u0442\u043A\u0430 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043B\u044F \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F \u043E\u0441\u0442\u0430\u0442\u043A\u043E\u0432 \u0441\u0442\u0430\u0440\u044B\u0445 \u043F\u043B\u0430\u0433\u0438\u043D\u043E\u0432 \u0438 \u0431\u043E\u043B\u0435\u0435 \u0440\u0430\u043D\u043D\u0438\u0445 \u0432\u0435\u0440\u0441\u0438\u0439. +OldDataMonitor.DisplayName=\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u043C\u0438 \u0434\u0430\u043D\u043D\u044B\u043C\u0438 +HudsonHomeDiskUsageMonitor.DisplayName=\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u043E\u0435 \u0434\u0438\u0441\u043A\u043E\u0432\u043E\u0435 \u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u043E +NullIdDescriptorMonitor.DisplayName=\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0434\u0435\u0441\u043A\u0440\u0438\u043F\u0442\u043E\u0440\u0430 +ReverseProxySetupMonitor.DisplayName=\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 Reverse Proxy \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Computer/sidepanel_zh_CN.properties b/core/src/main/resources/hudson/diagnosis/Messages_zh_CN.properties similarity index 64% rename from core/src/main/resources/hudson/model/Computer/sidepanel_zh_CN.properties rename to core/src/main/resources/hudson/diagnosis/Messages_zh_CN.properties index 0861540bb9d90ade7d7878db991e853dcf03ffcb..a35f0e0c6712d88121e94274612bed6fde2a3f60 100644 --- a/core/src/main/resources/hudson/model/Computer/sidepanel_zh_CN.properties +++ b/core/src/main/resources/hudson/diagnosis/Messages_zh_CN.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc. +# Copyright (c) 2018, suren # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,9 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -Back\ to\ List=\u8fd4\u56de\u5217\u8868 -Build\ History=\u6784\u5efa\u5386\u53f2 -Configure=\u914D\u7F6E\u4ECE\u8282\u70B9 -Load\ Statistics=\u8d1f\u8f7d\u7edf\u8ba1 -Script\ Console=\u811a\u672c\u547d\u4ee4\u884c -Status=\u72b6\u6001 +MemoryUsageMonitor.USED=\u5DF2\u7528 +MemoryUsageMonitor.TOTAL=\u603B\u5171 +OldDataMonitor.Description=\u4ECE\u65E7\u7684\u3001\u65E9\u671F\u7248\u672C\u7684\u63D2\u4EF6\u4E2D\u6E05\u7406\u914D\u7F6E\u6587\u4EF6\u3002 +OldDataMonitor.DisplayName=\u7BA1\u7406\u65E7\u6570\u636E +HudsonHomeDiskUsageMonitor.DisplayName=\u78C1\u76D8\u4F7F\u7528\u76D1\u63A7 + +NullIdDescriptorMonitor.DisplayName=\u63CF\u8FF0ID\u4E3A\u7A7A +ReverseProxySetupMonitor.DisplayName=\u53CD\u5411\u4EE3\u7406\u8BBE\u7F6E +TooManyJobsButNoView.DisplayName=\u4EFB\u52A1\u8FC7\u591A\u800C\u6CA1\u6709\u7EC4\u7EC7\u5230\u89C6\u56FE\u4E2D diff --git a/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.jelly b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.jelly index d194fc95f75e9da24dcdf595cf2cb4d09435d79c..610d36a6be8b289b63c5422a7453e2de48af4b25 100644 --- a/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.jelly +++ b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.jelly @@ -24,14 +24,12 @@ THE SOFTWARE. -
- ${%blurb} -
    - -
  • - ${%problem(d, d.displayName, app.pluginManager.whichPlugin(d.getClass()))} -
  • -
    -
-
+
+
+
${%blurb}
+ +
${%problem(d, d.displayName, app.pluginManager.whichPlugin(d.getClass()))}
+
+
+
diff --git a/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.properties b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.properties index 399938b97a620a2667a145542d9aa4929a60fdaf..60fb060dffd056221ac14a3ac415a910a384ac11 100644 --- a/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.properties +++ b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message.properties @@ -1,3 +1,3 @@ blurb=The following extensions have no ID value and therefore likely cause a problem. Please upgrade these plugins if they are not the latest, \ - and if they are the latest, please file a bug so that we can fix them. + and if they are the latest, please file a bug so that we can fix them problem=Descriptor {0} from plugin {2} with display name {1} \ No newline at end of file diff --git a/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message_bg.properties b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..82eaa1e162ffa3ce881dc4a1f2b3db46cb010841 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message_bg.properties @@ -0,0 +1,31 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, Alexander Shopov +# +# 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. + +# The following extensions have no ID value and therefore likely cause a problem. Please upgrade these plugins if they are not the latest, \ +# and if they are the latest, please file a bug so that we can fix them. +blurb=\ + \u0421\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0438 \u0441\u0430 \u0431\u0435\u0437 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 (ID), \u043a\u043e\u0435\u0442\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u0434\u043e\u0432\u0435\u0434\u0435 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438.\ + \u041e\u0431\u043d\u043e\u0432\u0435\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0438\u0442\u0435 \u043a\u044a\u043c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043c \u0432\u0435\u0440\u0441\u0438\u044f. \u0410\u043a\u043e \u0432\u0435\u0447\u0435 \u0441\u0430 \u043e\u0431\u043d\u043e\u0432\u0435\u043d\u0438, \u043c\u043e\u043b\u0438\u043c \u0434\u0430\ + \u043f\u043e\u0434\u0430\u0434\u0435\u0442\u0435 \u0434\u043e\u043a\u043b\u0430\u0434 \u0437\u0430 \u0433\u0440\u0435\u0448\u043a\u0430, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u043c\u0435 \u0442\u043e\u0432\u0430. +# Descriptor {0} from plugin {2} with display name {1} +problem=\ + \u0414\u0435\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0440 {0} \u043e\u0442 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430\u0442\u0430 {2} \u0441 \u0438\u043c\u0435 {1} diff --git a/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message_it.properties b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message_it.properties new file mode 100644 index 0000000000000000000000000000000000000000..d968c985079f45827261dcd2f8f1f61d5ab9f752 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/NullIdDescriptorMonitor/message_it.properties @@ -0,0 +1,3 @@ +blurb=Le seguenti estensioni non hanno un ID e pertanto probabilmente causeranno un problema. Aggiornare questi plugin se non sono all''ultima versione, \ +e se sono l''ultima versione, segnalare l''errore in modo che possiamo correggerli. +problem=Descrittore {0} del plugin {2} con nome visualizzato {1} diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage.jelly b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage.jelly index 0f73ec6d0fcba592507f0ae191b646d8e79d6b02..56587c3092ecad6a9c7c4b110869e8db9e19dd02 100644 --- a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage.jelly +++ b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage.jelly @@ -34,15 +34,22 @@ THE SOFTWARE. ${%Type}${%Name}${%Version} - - - ${range} + ${obj.class.name} ${obj.fullName?:obj.fullDisplayName?:obj.displayName?:obj.name} - ${range} + + + + ${item.value} + + + ${item.value} + + + ${item.value.extra} diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_bg.properties b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..ab631fc123448b2757c692d1831c1fd8789a5cd4 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_bg.properties @@ -0,0 +1,105 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, Alexander Shopov +# +# 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. + +Name=\ + \u0418\u043c\u0435 +# \ +# Eventually the code supporting these data migrations may be removed. Compatibility will be \ +# retained for at least 150 releases since the structure change. Versions older than this are \ +# in bold above, and it is recommended to resave these files. +blurb.4=\ + \u0421\u043b\u0435\u0434 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e \u0432\u0440\u0435\u043c\u0435 \u043a\u043e\u0434\u044a\u0442 \u0437\u0430 \u0442\u0435\u0437\u0438 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438 \u0449\u0435 \u0431\u044a\u0434\u0435 \u0438\u0437\u0442\u0440\u0438\u0442.\ + \u0421\u044a\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u0449\u0435 \u0431\u044a\u0434\u0435 \u0437\u0430\u043f\u0430\u0437\u0435\u043d\u0430 \u0437\u0430 \u043f\u043e\u043d\u0435 150 \u0438\u0437\u0434\u0430\u043d\u0438\u044f \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u043c\u044f\u043d\u0430\u0442\u0430 \u043f\u043e\ + \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u0442\u0430. \u0412\u0435\u0440\u0441\u0438\u0438\u0442\u0435, \u043f\u043e \u0440\u0430\u043d\u043d\u0438 \u043e\u0442 \u0442\u043e\u0432\u0430, \u0441\u0430 \u0432 \u043f\u043e\u043b\u0443\u0447\u0435\u0440\u043d\u043e. \u0417\u0430 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043d\u0435 \u0435 \u0434\u0430\ + \u0437\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u043d\u0430\u043d\u043e\u0432\u043e. +Upgrade=\ + \u041e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 +Manage\ Old\ Data=\ + \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u0441\u0442\u0430\u0440\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 +Discard\ Unreadable\ Data=\ + \u041e\u0442\u0445\u0432\u044a\u0440\u043b\u044f\u043d\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438\u0442\u0435, \u043a\u043e\u0438\u0442\u043e \u043d\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043f\u0440\u043e\u0447\u0435\u0442\u0430\u0442 +# \ +# When there are changes in how data is stored on disk, Jenkins uses the following strategy: \ +# data is migrated to the new structure when it is loaded, but the file is not resaved in the \ +# new format. This allows for downgrading Jenkins if needed. However, it can also leave data \ +# on disk in the old format indefinitely. The table below lists files containing such data, \ +# and the Jenkins version(s) where the data structure was changed. +blurb.1=\ + \u041f\u0440\u0438 \u043f\u0440\u043e\u043c\u044f\u043d\u0430 \u0432 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 Jenkins \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044f:\ + \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0441\u0435 \u043c\u0438\u0433\u0440\u0438\u0440\u0430\u0442 \u043a\u044a\u043c \u043d\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438 \u0437\u0430\u0440\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0444\u0430\u0439\u043b\u0430, \u043d\u043e \u0442\u043e\u0439\ + \u043e\u0441\u0442\u0430\u0432\u0430 \u0441\u044a\u0441 \u0441\u0442\u0430\u0440\u0430\u0442\u0430 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430. \u0422\u043e\u0432\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0432\u0430 \u0432\u0440\u044a\u0449\u0430\u043d\u0435 \u043a\u044a\u043c \u043f\u0440\u0435\u0434\u0438\u0448\u043d\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430\ + Jenkins, \u0430\u043a\u043e \u0441\u0435 \u043d\u0430\u043b\u0430\u0433\u0430. \u0422\u043e\u0432\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0432\u0430 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0434\u0430 \u043e\u0441\u0442\u0430\u043d\u0430\u0442 \u0432 \u0441\u0442\u0430\u0440\u0438\u044f \u0444\u043e\u0440\u043c\u0430\u0442 \u0437\u0430\ + \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u043d\u043e \u0434\u044a\u043b\u0433\u043e \u0432\u0440\u0435\u043c\u0435. \u0422\u0430\u0431\u043b\u0438\u0446\u0430\u0442\u0430 \u043f\u043e-\u0434\u043e\u043b\u0443 \u0441\u044a\u0434\u044a\u0440\u0436\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u0441 \u0442\u0430\u043a\u0438\u0432\u0430 \u0434\u0430\u043d\u043d\u0438,\ + \u043a\u0430\u043a\u0442\u043e \u0438 \u0432\u0435\u0440\u0441\u0438\u044f\u0442\u0430 \u043d\u0430 Jenkins, \u043f\u0440\u0438 \u043a\u043e\u044f\u0442\u043e \u0435 \u0441\u043c\u0435\u043d\u0435\u043d \u0444\u043e\u0440\u043c\u0430\u0442\u044a\u0442 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438\u0442\u0435. +# \ +# (downgrade as far back as the selected version may still be possible) +blurb.5=\ + (\u0432\u0440\u044a\u0449\u0430\u043d\u0435 \u043a\u044a\u043c \u043d\u0430\u0439-\u0441\u0442\u0430\u0440\u0430\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u0430 \u043e\u0442 \u0438\u0437\u0431\u0440\u0430\u043d\u0430\u0442\u0430) +# \ +# The form below may be used to resave these files in the current format. Doing so means a \ +# downgrade to a Jenkins release older than the selected version will not be able to read the \ +# data stored in the new format. Note that simply using Jenkins to create and configure jobs \ +# and run builds can save data that may not be readable by older Jenkins releases, even when \ +# this form is not used. Also if any unreadable data errors are reported in the right side \ +# of the table above, note that this data will be lost when the file is resaved. +blurb.3=\ + \u0424\u043e\u0440\u043c\u0443\u043b\u044f\u0440\u044a\u0442 \u043f\u043e-\u0434\u043e\u043b\u0443 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0432\u0430 \u0434\u0430 \u043f\u0440\u0435\u0437\u0430\u043f\u0438\u0448\u0435\u0442\u0435 \u0442\u0435\u0437\u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0432 \u043d\u043e\u0432\u0438\u044f \u0444\u043e\u0440\u043c\u0430\u0442.\ + \u0410\u043a\u043e \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u0435 \u0442\u043e\u0432\u0430 \u0438 \u0432\u044a\u0440\u043d\u0435\u0442\u0435 Jenkins \u043a\u044a\u043c \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u0435\u0434\u0438 \u0438\u0437\u0431\u0440\u0430\u043d\u0430\u0442\u0430, \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u043d\u044f\u043c\u0430\ + \u0434\u0430 \u0441\u0435 \u043f\u0440\u043e\u0447\u0435\u0442\u0430\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e. \u0414\u043e\u0440\u0438 \u0431\u0435\u0437 \u0434\u0430 \u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u0444\u043e\u0440\u043c\u0443\u043b\u044f\u0440 \u0435 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u043d\u0435\u0449\u043e\ + \u043f\u043e\u0434\u043e\u0431\u043d\u043e \u0434\u0430 \u0441\u0435 \u0441\u043b\u0443\u0447\u0438 \u2014 \u0430\u043a\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u0442\u0435 \u043d\u043e\u0432\u0438 \u0437\u0430\u0434\u0430\u043d\u0438\u044f, \u043f\u0440\u043e\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043b\u0438\ + \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435 \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0438. \u0410\u043a\u043e \u0432 \u0434\u044f\u0441\u043d\u0430\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0430 \u043d\u0430 \u0433\u043e\u0440\u043d\u0430\u0442\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0441\u0430\ + \u0434\u043e\u043a\u043b\u0430\u0434\u0432\u0430\u043d\u0438 \u0433\u0440\u0435\u0448\u043a\u0438 \u043f\u0440\u0438 \u0447\u0435\u0442\u0435\u043d\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438, \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u0438\u0437\u0433\u0443\u0431\u0435\u043d\u0438, \u0430\u043a\u043e\ + \u043f\u0440\u0435\u0437\u0430\u043f\u0438\u0448\u0435\u0442\u0435 \u0444\u0430\u0439\u043b\u0430. +# \ +# It is acceptable to leave unreadable data in these files, as Jenkins will safely ignore it. \ +# To avoid the log messages at Jenkins startup you can permanently delete the unreadable data \ +# by resaving these files using the button below. +blurb.6=\ + \u041d\u0435 \u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0434\u0430 \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438, \u043a\u043e\u0438\u0442\u043e \u043d\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043f\u0440\u043e\u0447\u0435\u0442\u0430\u0442 \u0432 \u0442\u0435\u0437\u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u0435,\ + \u0437\u0430\u0449\u043e\u0442\u043e Jenkins \u0449\u0435 \u0433\u0438 \u043f\u0440\u0435\u0441\u043a\u043e\u0447\u0438. \u0410\u043a\u043e \u043d\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u043f\u043e\u0432\u0435\u0447\u0435 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u0442\u0435 \u0442\u0435\u0437\u0438\ + \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0435\u0442\u0435 \u043d\u0435\u0447\u0435\u0442\u0438\u043c\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u043a\u0430\u0442\u043e \u043f\u0440\u0435\u0437\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u0441\ + \u0431\u0443\u0442\u043e\u043d\u0430 \u043e\u0442\u0434\u043e\u043b\u0443. +Unreadable\ Data=\ + \u0414\u0430\u043d\u043d\u0438, \u043a\u043e\u0438\u0442\u043e \u043d\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043f\u0440\u043e\u0447\u0435\u0442\u0430\u0442 +No\ old\ data\ was\ found.=\ + \u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0441\u0442\u0430\u0440\u0438 \u0434\u0430\u043d\u043d\u0438. +Version=\ + \u0412\u0435\u0440\u0441\u0438\u044f +Resave\ data\ files\ with\ structure\ changes\ no\ newer\ than\ Jenkins=\ + \u041f\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u0441 \u043f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u043e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0435 \u043f\u043e \u043d\u043e\u0432\u0438, \u043e\u0442 \u0442\u0435\u043a\u0443\u0449\u0430\u0442\u0430\ + \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Jenkins. +Error=\ + \u0413\u0440\u0435\u0448\u043a\u0430 +Type=\ + \u0412\u0438\u0434 +# \ +# Sometimes errors occur while reading data (if a plugin adds some data and that plugin is \ +# later disabled, if migration code is not written for structure changes, or if Jenkins is \ +# downgraded after it has already written data not readable by the older version). \ +# These errors are logged, but the unreadable data is then skipped over, allowing Jenkins to \ +# startup and function properly. +blurb.2=\ + \u0412 \u043d\u044f\u043a\u043e\u0438 \u0441\u043b\u0443\u0447\u0430\u0438 \u0432\u044a\u0437\u043d\u0438\u043a\u0432\u0430\u0442 \u0433\u0440\u0435\u0448\u043a\u0438 \u043f\u0440\u0438 \u0447\u0435\u0442\u0435\u043d\u0435\u0442\u043e \u043d\u0430 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 (\u043d\u0430\u043f\u0440. \u0430\u043a\u043e \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430\ + \u0434\u043e\u0431\u0430\u0432\u0438 \u0434\u0430\u043d\u043d\u0438, \u043d\u043e \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0431\u0438\u0432\u0430 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u0430, \u0430\u043a\u043e \u043e\u0449\u0435 \u043d\u0435 \u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043d \u043a\u043e\u0434 \u0437\u0430\ + \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u043b\u0438 \u0430\u043a\u043e Jenkins \u0431\u0438\u0432\u0430 \u0441\u043c\u0435\u043d\u0435\u043d \u0441 \u043f\u043e-\u0441\u0442\u0430\u0440\u0430 \u0432\u0435\u0440\u0441\u0438\u044f, \u043d\u043e \u043d\u043e\u0432\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435\ + \u0437\u0430\u043f\u0438\u0441\u0430\u043b\u0430 \u0434\u0430\u043d\u043d\u0438). \u0422\u0435\u0437\u0438 \u0433\u0440\u0435\u0448\u043a\u0438 \u0441\u0435 \u0432\u043f\u0438\u0441\u0432\u0430\u0442 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0438\u0442\u0435, \u043d\u043e \u0434\u0430\u043d\u043d\u0438\u0442\u0435, \u043a\u043e\u0438\u0442\u043e \u043d\u0435\ + \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043f\u0440\u043e\u0447\u0435\u0442\u0430\u0442, \u0441\u0435 \u043f\u0440\u0435\u0441\u043a\u0430\u0447\u0430\u0442, \u0437\u0430 \u0434\u0430 \u043c\u043e\u0436\u0435 Jenkins \u0434\u0430 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430 \u043d\u043e\u0440\u043c\u0430\u043b\u043d\u043e. diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_it.properties b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_it.properties index b995623eab7c9d6d6a4f109c19765e5ac594c7c7..a92c6c8caf48420a20e3fb17358e4114fe205018 100644 --- a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_it.properties +++ b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_it.properties @@ -1,9 +1,47 @@ -# This file is under the MIT License by authors - -Manage\ Old\ Data=Gestici dati vecchi -Name=Nome -No\ old\ data\ was\ found.=Nessun vecchio dato trovato. +blurb.1=\ + Quando vi sono dei cambiamenti nel modo in cui i dati sono salvati su disco, Jenkins \ + utilizza la seguente strategia: i dati sono migrati alla loro nuova struttura quando \ + vengono caricati, ma il file non salvato nuovamente nel nuovo formato. Ci consente \ + di effettuare il downgrade di Jenkins se necessario. Tuttavia, ci pu anche lasciare \ + dei dati su disco nel vecchio formato indefinitamente. La tabella sottostante elenca \ + i file che contengono tali dati e le versioni di Jenkins in cui stata modificata la \ + struttura dati. +blurb.2=\ + A volte si verificano errori durante la lettura dei dati (se un plugin aggiunge dei dati \ + e tale plugin viene disabilitato in un secondo momento, se non stato scritto codice per \ + gestire la migrazione quando vengono modificate le strutture dati, o se si esegue il \ + downgrade di Jenkins dopo che questo ha gi scritto dati non leggibili dalla vecchia \ + versione). Questi errori sono registrati, ma i dati non leggibili vengono saltati, \ + consentendo a Jenkins di avviarsi e funzionare correttamente. +blurb.3=\ + Il modulo sottostante pu essere utilizzato per salvare nuovamente questi file nel \ + formato corrente. Eseguire tale operazione comporter che un downgrade a una versione di \ + Jenkins pi vecchie della versione selezionata non sar in grado di leggere i dati \ + memorizzati nel nuovo formato. Si noti che il semplice utilizzo di Jenkins per creare \ + e configurare processi ed eseguire build pu salvare dati che potrebbero non essere \ + leggibili da versioni di Jenkins pi vecchie, anche nel caso in cui non si utilizzi \ + questo modulo. Inoltre, se vi sono degli errori relativi a dati non leggibili nella \ + parte destra della tabella soprastante, si noti che tali dati andranno perduti quando il \ + file sar salvato nuovamente. +blurb.4=\ + Prima o poi il codice per il supporto di queste migrazioni dati potrebbe essere rimosso. \ + La compatibilit sar mantenuta per almeno 150 versioni a partire dalla modifica della \ + struttura. Le versioni pi vecchie di queste sono evidenziate sopra in grassetto, e si \ + raccomanda di salvare nuovamente questi file. +blurb.5=\ + (potrebbe essere ancora possibile eseguire il downgrade fino alla versione selezionata) +blurb.6=\ + accettabile lasciare dati non leggibili in tali file, in quanto Jenkins li ignorer \ + in modo sicuro. Per evitare i messaggi di log all''avvio di Jenkins si possono eliminare \ + definitivamente i dati non leggibili salvando nuovamente questi file utilizzando il \ + pulsante sottostante. +Manage\ Old\ Data=Gestisci dati vecchi Type=Tipo +Name=Nome Version=Versione -blurb.1=Quando ci sono cambiamenti nel modo in cui i dati sono salvati sul disco, Jenkins usa la seguente strategia: i dati sono migrati nella nuova struttura quando caricati, ma i file non sono salvati nel nuovo formato. Questo consente di fare il downgrade di Jenkins se necessario. Comunque, lascia anche i dati sul disco nel vecchio formato indefinitamente. La tabella sotto mostra la lista dei file contenenti tali dati, e le versioni di Jenkins dove la struttura dei dati \u00E8 stata cambiata. -blurb.2=Qualche volta capitano degli errori durante la lettura dei dati (se un plugin aggiunge alcuni dati e quel plugin viene successivamente disabilitato, se il codice di migrazione non \u00E8 scritto per cambiamenti strutturali o se a Jenkins \u00E8 fatto il downgrade dopo che ha gi\u00E0 scritto dati non leggibili nella vecchia versione). Questi errori sono inseriti nel log, ma i dati non leggibili vengono saltati, permettendo a Jenkins di partire e funzionare correttamente. +Resave\ data\ files\ with\ structure\ changes\ no\ newer\ than\ Jenkins=Salva nuovamente file dati con modifiche strutturali non introdotte in una versione di Jenkins superiore alla +Upgrade=Aggiorna +No\ old\ data\ was\ found.=Nessun dato vecchio trovato. +Unreadable\ Data=Dati illeggibili +Error=Errore +Discard\ Unreadable\ Data=Elimina dati illeggibili diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_zh_CN.properties b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_zh_CN.properties deleted file mode 100644 index 1ebe0c9ff05879e9e55892613ddc8fa28f288eaa..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/manage_zh_CN.properties +++ /dev/null @@ -1,7 +0,0 @@ -# This file is under the MIT License by authors - -Manage\ Old\ Data=\u7BA1\u7406\u65E7\u6570\u636E -Name=\u540D\u79F0 -No\ old\ data\ was\ found.=\u672A\u627E\u5230\u65E7\u6570\u636E -Type=\u7C7B\u578B -Version=\u7248\u672C diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message.jelly b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message.jelly index 89d38c592e5033a930880788ac2fefa43913212b..c213f7ac2251257473f00400291638c440c6c3a4 100644 --- a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message.jelly +++ b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message.jelly @@ -24,13 +24,11 @@ THE SOFTWARE. -
+
- ${%You have data stored in an older format and/or unreadable data.} - - + ${%You have data stored in an older format and/or unreadable data.}
diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_bg.properties b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_bg.properties new file mode 100644 index 0000000000000000000000000000000000000000..b8566357e90efa45431a6675581180e717558b56 --- /dev/null +++ b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_bg.properties @@ -0,0 +1,29 @@ +# The MIT License +# +# Bulgarian translation: Copyright (c) 2016, Alexander Shopov +# +# 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. + +Dismiss=\ + \u041e\u0442\u043a\u0430\u0437 +Manage=\ + \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 +You\ have\ data\ stored\ in\ an\ older\ format\ and/or\ unreadable\ data.=\ + \u0427\u0430\u0441\u0442 \u043e\u0442 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u043d\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u043d\u0438, \u0444\u043e\u0440\u043c\u0430\u0442\u044a\u0442 \u0438\u043c \u0435 \u043f\u0440\u0435\u043a\u0430\u043b\u0435\u043d\u043e \u0441\u0442\u0430\u0440 \u0438\u043b\u0438\ + \u0435 \u0441\u0433\u0440\u0435\u0448\u0435\u043d. diff --git a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_it.properties b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_it.properties index 219dc22daf36d42c2627103342f2d823081d5db2..5ce17631a4d8391040fb7dc19b539f4f5c8a1dd1 100644 --- a/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_it.properties +++ b/core/src/main/resources/hudson/diagnosis/OldDataMonitor/message_it.properties @@ -1,25 +1,4 @@ -# The MIT License -# -# Copyright (c) 2004-2010, Sun Microsystems, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Dismiss=Chiudi +Dismiss=Ignora Manage=Gestisci -You\ have\ data\ stored\ in\ an\ older\ format\ and/or\ unreadable\ data.=Sono presenti dati salvati in un formato precedente o dati illeggibili. +You\ have\ data\ stored\ in\ an\ older\ format\ and/or\ unreadable\ data.=\ + Alcuni dati sono salvati in un vecchio formato e/o sono illeggibili. diff --git a/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message.jelly b/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message.jelly index a39c1238b0d4244169ed8bad6b5fa89f23f6bdf2..e2aee20effddbdf421dbbd66cc2830eb512fc55b 100644 --- a/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message.jelly +++ b/core/src/main/resources/hudson/diagnosis/ReverseProxySetupMonitor/message.jelly @@ -27,7 +27,9 @@ THE SOFTWARE. -