diff --git a/.gitignore b/.gitignore index 60ba89065c90d09f0cc1ea8935b0bf08b691b0e9..3cde4d575dc5ceede4290298480ac31d0eb8875b 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,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..ab12568a0a3b58e3b1789651e0d0f0d3343556df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,115 @@ # 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 https://jenkins.io/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](http://openjdk.java.net/), + but you can use other JDKs as well. + - Java 9 is **not supported** in Jenkins. + * Maven 3.5.3 or above. You can download it [here](https://maven.apache.org/download.cgi) + * Any IDE which supports importing Maven projects +4. Setup your development environment as described in [Preparing for Plugin Development](https://jenkins.io/doc/developer/tutorial/prepare/) + +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](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)). + +## Building and Debugging + +The build flow for Jenkins core is built around Maven. +Building and debugging process is described [here](https://jenkins.io/doc/developer/building/). + +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](https://wiki.jenkins.io/display/JENKINS/Starting+and+Accessing+Jenkins)). +If you want to debug this WAR file without using Maven plugins, +You can just start the executable with [Remote Debug Flags](https://stackoverflow.com/questions/975271/remote-debugging-a-java-application) +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)](https://github.com/jenkinsci/acceptance-test-harness) 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 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. + The backporting process is described [here](https://jenkins.io/download/lts/). +4. Fill in the Pull Request description according to the [proposed template](.github/PULL_REQUEST_TEMPLATE.md). +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](./LICENSE.txt), 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](https://wiki.jenkins.io/display/JENKINS/Copyright+on+source+code) +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](https://jenkins.io/security/#team). + +## Continuous Integration + +The Jenkins project has a Continuous Integration server... powered by Jenkins, of course. +It is located at [ci.jenkins.io](https://ci.jenkins.io/). + +The Jenkins project uses [Jenkins Pipeline](https://jenkins.io/doc/book/pipeline/) to run builds. +The code for the core build flow is stored in the [Jenkinsfile](./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)) + 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 bcced2b016045b0c7be4012dc213c1ccbdc9d6da..f6eb3e2c0cfdc06c586ea5c1a1a46689b9811957 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] 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,16 +33,20 @@ 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' @@ -51,34 +58,63 @@ for(i = 0; i < buildTypes.size(); i++) { } // 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' + } + 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 +128,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/cli/pom.xml b/cli/pom.xml index d5aa1608e7024213a069ccbf9c2f98fe142b2a6e..670e416a26dd5c5f5bc6943ad046e9f8c4c41c00 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -4,8 +4,8 @@ org.jenkins-ci.main - pom - 2.73.4-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,7 +63,7 @@ org.apache.sshd sshd-core - 1.6.0 + 1.7.0 true @@ -68,6 +76,15 @@ trilead-ssh2 build214-jenkins-1 + + com.google.code.findbugs + annotations + provided + + + commons-lang + commons-lang + @@ -78,17 +95,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 0d47f36ff90f73125fe6c04f9045af76a42c3a44..52c6785d5871d2e447b6747fd98eb88e1ff50bb9 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. @@ -452,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()) { @@ -549,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()); @@ -563,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 += '/'; } @@ -809,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 ef8bd5ff12cea23a0906ce9e07a6fa7843973d70..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; } @@ -79,11 +85,10 @@ public class FullDuplexHttpStream { URL target = new URL(this.base, relativeTarget); - CrumbData crumbData = new CrumbData(); - 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 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,11 +117,8 @@ 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"); } // As this transport mode is using POST, it is necessary to resolve possible redirections using GET first. @@ -141,59 +142,4 @@ public class FullDuplexHttpStream { 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(); - if (authorization != null) { - con.addRequestProperty("Authorization", authorization); - } - CLI.verifyJenkinsConnection(con); - - 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(); - } - } - } } 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 bdd6c2beb50e5e696af3467e5bae7598f41eb5ef..39dc9210a3e1615bac888932c689d97ad4dd1b4d 100644 --- a/cli/src/main/java/hudson/cli/SSHCLI.java +++ b/cli/src/main/java/hudson/cli/SSHCLI.java @@ -51,6 +51,8 @@ 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}. * In a separate class to avoid any class loading of {@code sshd-core} when not using {@code -ssh} mode. @@ -59,6 +61,7 @@ import org.apache.sshd.common.util.security.SecurityUtils; */ 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"); 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 9a582a5af472d4c477e527ca422b656f95d277df..cbbf3f30616771fbd074ee27f7f709e815ef576c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -28,8 +28,8 @@ THE SOFTWARE. org.jenkins-ci.main - pom - 2.73.4-SNAPSHOT + jenkins-parent + ${revision}${changelist} jenkins-core @@ -39,11 +39,9 @@ THE SOFTWARE. true - 1.250 + 1.255 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 @@ -173,6 +177,12 @@ THE SOFTWARE. tests test + + org.hamcrest + hamcrest-library + 1.3 + test + com.infradna.tool @@ -197,17 +207,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 +243,20 @@ THE SOFTWARE. + + + xpp3 + xpp3 + 1.1.4c + + + net.sf.kxml + kxml2 + 2.3.0 + jfree jfreechart @@ -256,7 +279,6 @@ THE SOFTWARE. commons-lang commons-lang - 2.6 commons-digester @@ -440,11 +462,6 @@ THE SOFTWARE. spring-aop ${spring.version} - - xpp3 - xpp3 - 1.1.4c - junit junit @@ -462,13 +479,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 +513,7 @@ THE SOFTWARE. org.jvnet.winp winp - 1.25 + 1.27 org.jenkins-ci @@ -516,7 +533,7 @@ THE SOFTWARE. net.java.dev.jna jna - 4.2.1 + 4.5.2 org.kohsuke @@ -526,7 +543,7 @@ THE SOFTWARE. org.kohsuke libpam4j - 1.8 + 1.11 org.kohsuke @@ -541,7 +558,7 @@ THE SOFTWARE. net.java.sezpoz sezpoz - 1.12 + 1.13 org.kohsuke.jinterop @@ -551,8 +568,7 @@ THE SOFTWARE. org.kohsuke.metainf-services metainf-services - 1.4 - provided + 1.8 true @@ -566,10 +582,9 @@ THE SOFTWARE. 1.1 - + commons-codec commons-codec - 1.8 @@ -580,7 +595,6 @@ THE SOFTWARE. com.google.code.findbugs annotations - 3.0.0 provided @@ -602,6 +616,12 @@ THE SOFTWARE. com.google.guava guava + + + com.google.code.findbugs + jsr305 + + com.google.guava @@ -621,6 +641,7 @@ THE SOFTWARE. org.codehaus.mojo build-helper-maven-plugin + 1.7 add-source @@ -785,25 +806,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - - - - - testCompile - - - - - - org.codehaus.groovy - groovy-all - ${groovy.version} - - - org.codehaus.mojo findbugs-maven-plugin @@ -878,41 +880,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..dd334db858be47f551cf25a897d37b11d095fae5 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; @@ -213,14 +214,12 @@ public class DescriptorExtensionList, D extends Descrip /** * 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..cd553f3aba01ea325c58912b990d88aab9cb5e66 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 TODO + * @return The platform. + */ + public @CheckForNull Platform getPlatform() { + return platform; + } + /** + * Sets the platform for which these env vars target. + * @since TODO + * @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..9452064e8105e124c0e62328f3310c03765b4ff7 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -58,6 +58,9 @@ import hudson.util.IOUtils; import hudson.util.NamingThreadFactory; import hudson.util.io.Archiver; import hudson.util.io.ArchiverFactory; + +import static java.util.logging.Level.FINE; + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; @@ -77,11 +80,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.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 +117,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; @@ -125,9 +136,12 @@ 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 +228,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 +283,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 +315,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 +480,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 +503,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 +525,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,24 +566,37 @@ 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; + } /** * Reads the given InputStream as a zip file and extracts it into this directory. @@ -555,13 +608,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 +645,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 +679,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 +698,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 +724,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 +795,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 +963,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 +1070,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 +1172,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 +1201,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,45 +1241,52 @@ 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 { + deleteRecursive(deleting(f)); + 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 class DeleteContents extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + deleteContentsRecursive(f); + return null; + } } private void deleteRecursive(File dir) throws IOException { @@ -1305,17 +1397,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 +1461,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 +1512,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 +1560,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 +1594,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,8 +1610,15 @@ 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 { @@ -1482,12 +1631,22 @@ public final class FilePath implements Serializable { 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 +1659,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 +1681,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 +1696,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 +1711,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 +1726,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 +1747,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 +1801,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 +1853,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 +1874,6 @@ public final class FilePath implements Serializable { return r; } - }, (filter!=null?filter:this).getClass().getClassLoader()); } /** @@ -1731,8 +1917,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 +1939,6 @@ public final class FilePath implements Serializable { return r; } - }); } /** @@ -1757,7 +1953,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; } @@ -1775,25 +1976,29 @@ public final class FilePath implements Serializable { } 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(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; + } + } /** * Reads this file from the specific offset. @@ -1892,8 +2097,11 @@ public final class FilePath implements Serializable { } } - 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()); @@ -1904,7 +2112,6 @@ public final class FilePath implements Serializable { throw new IOException(e); } } - }); } /** @@ -1915,19 +2122,27 @@ public final class FilePath implements Serializable { * @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(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; + } } /** @@ -1935,12 +2150,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 +2168,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 +2192,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 +2209,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 +2218,6 @@ public final class FilePath implements Serializable { deleting(tmp).delete(); return null; } - }); } /** @@ -2009,11 +2238,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 +2271,34 @@ 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(reading(f).toPath())) { + org.apache.commons.io.IOUtils.copy(fis, out); + return null; + } catch (InvalidPathException e) { + throw new IOException(e); + } 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 +2306,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 +2392,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 +2425,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 +2604,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 +2684,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 +2699,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 +2858,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 +3099,11 @@ public final class FilePath implements Serializable { return classLoader; } + @Override + public String toString() { + return callable.toString(); + } + private static final long serialVersionUID = 1L; } @@ -2789,11 +3128,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 +3264,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 +3280,60 @@ 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 parent = parentFile.getAbsoluteFile().toPath().normalize(); + + String remainingPath = potentialChildRelativePath; + File currentFile = parentFile; + while (!remainingPath.isEmpty()) { + File directChild = this.getDirectChild(currentFile, remainingPath); + File childUsingFullPath = new File(currentFile, remainingPath); + remainingPath = childUsingFullPath.getAbsolutePath().substring(directChild.getAbsolutePath().length()); + + File childFileSymbolic = Util.resolveSymlinkToFile(directChild); + if (childFileSymbolic == null) { + currentFile = directChild; + } else { + currentFile = childFileSymbolic; + } + } + + //TODO could be refactored using Util#isDescendant(File, File) from 2.80+ + Path child = currentFile.getAbsoluteFile().toPath().normalize(); + return child.startsWith(parent); + } + + private @CheckForNull File getDirectChild(File parentFile, String childPath){ + File current = new File(parentFile, childPath); + while (current != null && !parentFile.equals(current.getParentFile())) { + current = current.getParentFile(); + } + 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..ef2a7671a3f777f5f6a4b6c849b40799857ae636 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,14 @@ 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> getCloudDescriptors() { return Cloud.all(); } @@ -2024,7 +2049,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 +2068,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..ac2fcd5507ad4b1d33dcf2b4b112818bacfcf4ca 100644
    --- a/core/src/main/java/hudson/PluginManager.java
    +++ b/core/src/main/java/hudson/PluginManager.java
    @@ -26,6 +26,7 @@ package hudson;
     import edu.umd.cs.findbugs.annotations.NonNull;
     import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     import hudson.security.ACLContext;
    +import jenkins.ExtensionRefreshException;
     import jenkins.util.SystemProperties;
     import hudson.PluginWrapper.Dependency;
     import hudson.init.InitMilestone;
    @@ -88,6 +89,7 @@ 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;
    @@ -120,7 +122,6 @@ 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.Set;
     import java.util.TreeMap;
    @@ -144,12 +145,15 @@ import hudson.util.FormValidation;
     import java.io.ByteArrayInputStream;
     import java.net.JarURLConnection;
     import java.net.URLConnection;
    +import java.util.ServiceLoader;
    +import java.util.function.Supplier;
     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 jenkins.security.CustomClassFilter;
     import org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
     
    @@ -175,7 +179,7 @@ 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";
     
    @@ -254,7 +258,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.
    @@ -463,10 +467,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 +654,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 +693,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 +709,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 +872,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 +923,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");
             }
         }
    @@ -1132,9 +1151,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 +1216,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 +1493,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);
                             }
                         }
    @@ -1805,6 +1824,19 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
             return requestedPlugins;
         }
     
    +    @Restricted(DoNotUse.class) // table.jelly
    +    public MetadataCache createCache() {
    +        return new MetadataCache();
    +    }
    +
    +    @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.
          */
    @@ -2000,8 +2032,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 +2084,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..24d90aaa45849a80d2a53df79eff5d780869ecd3 100644
    --- a/core/src/main/java/hudson/PluginWrapper.java
    +++ b/core/src/main/java/hudson/PluginWrapper.java
    @@ -36,7 +36,9 @@ import jenkins.model.Jenkins;
     import hudson.model.UpdateCenter;
     import hudson.model.UpdateSite;
     import hudson.util.VersionNumber;
    -import org.jvnet.localizer.ResourceBundleHolder;
    +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;
    @@ -51,7 +53,6 @@ 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;
    @@ -61,15 +62,16 @@ import java.util.Collection;
     import java.util.Collections;
     import java.util.Enumeration;
     import java.util.HashMap;
    -import java.util.HashSet;
     import java.util.Iterator;
     import java.util.List;
     import java.util.Map;
     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 java.util.logging.Level.WARNING;
     import static org.apache.commons.io.FilenameUtils.getBaseName;
    @@ -79,7 +81,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 +126,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 +151,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 +161,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()));
         }
     
    -    private final transient List<String> dependencyErrors = new ArrayList<>();
    +    @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();
    +    }
    +
    +    @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?
    @@ -230,8 +256,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 +300,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;
    @@ -571,7 +597,7 @@ 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);
                     }
                 }
             }
    @@ -581,21 +607,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 +632,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 +642,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 +657,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.
    @@ -750,6 +796,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();
    diff --git a/core/src/main/java/hudson/ProxyConfiguration.java b/core/src/main/java/hudson/ProxyConfiguration.java
    index 3baea805d42d341ae5b37cc6e43469dde3e39070..b8ba5bdf3f2c55626bad094350771ff59561aedc 100644
    --- a/core/src/main/java/hudson/ProxyConfiguration.java
    +++ b/core/src/main/java/hudson/ProxyConfiguration.java
    @@ -204,7 +204,7 @@ 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));
    @@ -341,6 +341,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());
                 }
    diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java
    index 602bb31c724e278a5ccd9144292016805ec690ee..f0a9c037002dbbce2b2dabfaaf7ae07283b032e6 100644
    --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java
    +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java
    @@ -30,8 +30,11 @@ 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.slaves.RemotingVersionInfo;
     import jenkins.util.SystemProperties;
     import hudson.slaves.OfflineCause;
     import java.io.DataOutputStream;
    @@ -53,6 +56,7 @@ 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;
    @@ -63,7 +67,7 @@ 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.
    @@ -98,6 +102,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());
     
    @@ -155,7 +164,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) {
    @@ -194,12 +210,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
    @@ -266,6 +291,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");
    @@ -275,6 +301,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 {
    @@ -301,6 +328,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.
    @@ -383,6 +425,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.
          */
    @@ -397,10 +517,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")
    @@ -408,11 +528,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..8007200a6293ead38d91608592412fe13cae2d9c 100644
    --- a/core/src/main/java/hudson/Util.java
    +++ b/core/src/main/java/hudson/Util.java
    @@ -23,35 +23,26 @@
      */
     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 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 +55,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.NoSuchFileException;
     import java.nio.file.Path;
     import java.nio.file.Paths;
    +import java.nio.file.attribute.PosixFilePermission;
    +import java.nio.file.attribute.BasicFileAttributes;
    +import java.nio.file.attribute.PosixFileAttributes;
    +import java.nio.file.attribute.DosFileAttributes;
    +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;
     
     /**
      * Various utility methods that don't have more proper home.
    @@ -135,7 +138,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 +150,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 +187,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();
         }
     
         /**
    @@ -258,16 +285,16 @@ public class Util {
          * 
          * @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
    +     * @throws IOException if it exists but could not be successfully deleted,
    +     * or if it represents an invalid {@link Path}.
          */
         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;
    -
    +        Path path = fileToPath(f);
    +        try {
    +            Files.deleteIfExists(path);
    +        } catch (IOException e) {
                 // perhaps this file is read-only?
    -            makeWritable(f);
    +            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)
    @@ -281,56 +308,47 @@ public class Util {
                  $ 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);
    -                }
    -
    +            Path parent = path.getParent();
    +            if (parent != null) {
    +                makeWritable(parent);
    +            }
    +            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
                     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());
    +                    throw new IOException("Unable to delete " + f.getPath()+" - files in dir: "+Arrays.asList(files), e2);
    +                throw e2;
                 }
             }
         }
     
         /**
    -     * Makes the given file writable by any means possible.
    +     * Makes the file at the given path 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);
    +    private static void makeWritable(@Nonnull Path path) throws IOException {
    +        if (!Functions.isWindows()) {
    +            try {
    +                PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class);
    +                Set<PosixFilePermission> newPermissions = attrs.permissions();
    +                newPermissions.add(PosixFilePermission.OWNER_WRITE);
    +                Files.setPosixFilePermissions(path, newPermissions);
    +                return;
    +            } catch (NoSuchFileException e) {
    +                return;
    +            } catch (UnsupportedOperationException e) {
    +                // 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);
         }
     
         /**
    @@ -493,7 +511,6 @@ public class Util {
         /**
          * Checks if the given file represents a symlink.
          */
    -    //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 {
             /*
              *  Windows Directory Junctions are effectively the same as Linux symlinks to directories.
    @@ -502,44 +519,27 @@ 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
    +        try {
    +            Path path = fileToPath(file);
    +            BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    +            if (attrs.isSymbolicLink()) {
    +                return true;
    +            } else if (attrs instanceof DosFileAttributes) {
    +                /* Returns true for non-symbolic link reparse points and devices. We could call
    +                 * WindowsFileAttributes#isReparsePoint with reflection instead to exclude devices,
    +                 * but as mentioned in the above comment this does not appear to be an issue.
    +                 */
    +                return attrs.isOther();
    +            } else {
    +                return false;
                 }
    -        }
    -        Boolean r = isSymlinkJava7(file);
    -        if (r != null) {
    -            return r;
    -        }
    -        String name = file.getName();
    -        if (name.equals(".") || name.equals(".."))
    +        } catch (NoSuchFileException e) {
                 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);
             }
         }
     
    @@ -567,16 +567,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 +640,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 +685,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 +693,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 +702,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 +712,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 +802,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 +824,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 +839,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 +853,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 +880,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 +1012,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 +1075,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 +1112,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 +1169,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 +1195,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 TODO
    +     */
    +    @Nonnull
    +    public static <T> T fixNull(@CheckForNull T s, @Nonnull T defaultValue) {
    +        return s != null ? s : defaultValue;
         }
     
         /**
    @@ -1200,24 +1228,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 +1387,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 +1402,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 +1465,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 +1477,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 +1498,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,10 +1656,79 @@ 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()));
    +    }
    +    
         public static final FastDateFormat XS_DATETIME_FORMATTER = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'",new SimpleTimeZone(0,"GMT"));
     
         // Note: RFC822 dates must not be localized!
    @@ -1736,4 +1796,15 @@ public class Util {
          */
         @Restricted(value = NoExternalUse.class)
         static boolean GC_AFTER_FAILED_DELETE = SystemProperties.getBoolean(Util.class.getName() + ".performGCOnFailedDelete");
    +
    +    /**
    +     * 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/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java
    index d507662037b575cc82fab21ed0fcb21c9e764850..91d88118d1dcf99ca57a3bdc407c010e509b3d74 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
    @@ -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/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..2cd45a77766f373035938f234f3ec47215114b84 100644
    --- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
    +++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java
    @@ -22,6 +22,8 @@ import java.util.logging.Level;
     import java.util.logging.Logger;
     import jenkins.security.HMACConfidentialKey;
     
    +import javax.annotation.Nonnull;
    +
     /**
      * Represents the authentication credential store of the CLI client.
      *
    @@ -38,7 +40,7 @@ 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());
    -
    +    
         /**
          * Where the store should be placed.
          */
    @@ -51,16 +53,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,7 +66,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() {
    +    public @Nonnull Authentication get() {
             Jenkins h = Jenkins.getActiveInstance();
             String val = props.getProperty(getPropertyKey());
             if (val == null) {
    @@ -100,6 +93,7 @@ public class ClientAuthenticationCache implements Serializable {
                 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 +104,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();
         }
     
         /**
    @@ -146,4 +142,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/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/DeleteBuildsCommand.java b/core/src/main/java/hudson/cli/DeleteBuildsCommand.java
    index db86c433ab538a8317058766afdc4f4d39dd93b5..bd1768786cd4c5bfd6e9df37547eda439b6a8632 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
    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/HelpCommand.java b/core/src/main/java/hudson/cli/HelpCommand.java
    index 60fcc0970be1f6faa8badffcdfa0b1dde132a42c..7b73c0b31dfef2993e39d773cfc47ad959cc3b06 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)
    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..68c30dd0152d4af2f791a1a389d39ceb5bceb6a5 100644
    --- a/core/src/main/java/hudson/cli/ListJobsCommand.java
    +++ b/core/src/main/java/hudson/cli/ListJobsCommand.java
    @@ -66,7 +66,7 @@ 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).getAllItems(TopLevelItem.class);
                     }
                     // 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/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/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..0648f057a04b28ab7aff06b283e6159b36a93054 100644
    --- a/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java
    +++ b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java
    @@ -62,7 +62,7 @@ public abstract class GenericItemOptionHandler<T extends Item> extends OptionHan
             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/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..a572a966b614a2b8f10b451ecd8ae696eab6cbc2 100644
    --- a/core/src/main/java/hudson/console/ConsoleNote.java
    +++ b/core/src/main/java/hudson/console/ConsoleNote.java
    @@ -54,6 +54,7 @@ import com.jcraft.jzlib.GZIPOutputStream;
     import hudson.remoting.ClassFilter;
     import jenkins.security.HMACConfidentialKey;
     import jenkins.util.SystemProperties;
    +import org.jenkinsci.remoting.util.AnonymousClassWarnings;
     
     /**
      * Data that hangs off from a console output.
    @@ -108,7 +109,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.
    @@ -180,7 +181,7 @@ 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 (ObjectOutputStream oos = AnonymousClassWarnings.checkingObjectOutputStream(new GZIPOutputStream(buf))) {
                 oos.writeObject(this);
             }
     
    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/ReverseProxySetupMonitor.java b/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java
    index 38a9215c22aebd50389331a6ac5a6b6685f6b5fe..5e3563f3a4461fb57b60dd0f9a1b7e83bd9487d9 100644
    --- a/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java
    +++ b/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java
    @@ -92,7 +92,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/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..f15dd429ba7d4d07f1d377b74241857246d67399 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, Jenkins.get(), "/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 2748596197129a930f760e9b0d7b82e65bbe28d1..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() {
    @@ -436,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 {
    @@ -456,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()) {
    @@ -795,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
          */
    diff --git a/core/src/main/java/hudson/model/AbstractCIBase.java b/core/src/main/java/hudson/model/AbstractCIBase.java
    index 3cef84804b1e8c8afc9ee5c6f9c2690d77bf9fdb..d82afd6488d03d5e52f48abc3347332106d7f3b8 100644
    --- a/core/src/main/java/hudson/model/AbstractCIBase.java
    +++ b/core/src/main/java/hudson/model/AbstractCIBase.java
    @@ -205,9 +205,10 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
                     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.
    @@ -226,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 26be93863398833b13439469174c299a14934460..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() + ")");
    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/AdministrativeMonitor.java b/core/src/main/java/hudson/model/AdministrativeMonitor.java
    index 537f6d092ce63dd90cac459c1efb2da031ca2f71..16b3fbb69774c938aaf531c3d3cc53df353d5ab3 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>
    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 afad020673dda840a82e4277575e0037e1db6ed0..641d82fc27ea758fd2087abd86a19205adff31f7 100644
    --- a/core/src/main/java/hudson/model/Computer.java
    +++ b/core/src/main/java/hudson/model/Computer.java
    @@ -30,9 +30,7 @@ import hudson.EnvVars;
     import hudson.Extension;
     import hudson.Launcher.ProcStarter;
     import hudson.slaves.Cloud;
    -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 +64,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 +86,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 +147,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 +325,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 +333,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 +777,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 +906,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);
    @@ -1067,6 +1069,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 +1357,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 +1432,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 +1480,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 +1493,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..e89f28c8a5785c38230cf04b03fb1c0df701566d 100644
    --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java
    +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java
    @@ -29,11 +29,15 @@ 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.StringTokenizer;
    @@ -51,6 +55,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;
     
    @@ -213,7 +218,7 @@ public final class DirectoryBrowserSupport implements HttpResponse {
             String rest = _rest.toString();
     
             // this is the base file/directory
    -        VirtualFile baseFile = root.child(base);
    +        VirtualFile baseFile = base.isEmpty() ? root : root.child(base);
     
             if(baseFile.isDirectory()) {
                 if(zip) {
    @@ -295,6 +300,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();
     
    @@ -338,7 +351,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;
    @@ -355,7 +368,8 @@ public final class DirectoryBrowserSupport implements HttpResponse {
         private static void zip(OutputStream outputStream, VirtualFile dir, String glob) throws IOException {
             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
    @@ -404,12 +418,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 +474,29 @@ 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;
    +        }
    +
             private static final long serialVersionUID = 1L;
         }
     
    @@ -499,7 +550,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 +571,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 +586,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 +625,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 6c28c80d0fb419e4afe3714ced93128ec563a269..ada651bbe4c6b723538606f9d67ed173a7724d88 100644
    --- a/core/src/main/java/hudson/model/DownloadService.java
    +++ b/core/src/main/java/hudson/model/DownloadService.java
    @@ -35,7 +35,7 @@ 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;
    @@ -132,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;
         }
     
    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 a69aea9156d071d7e55ef5c5dff2e0782f96d98a..3b160ae9270b311fe8e1a96e04bd886ab01c65eb 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;
    @@ -87,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;
    @@ -105,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.
    @@ -138,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;
         }
     
    @@ -396,6 +402,8 @@ public class Executor extends Thread implements ModelObject {
                         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()});
    @@ -423,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 {
    @@ -481,6 +490,7 @@ public class Executor extends Thread implements ModelObject {
             if (this instanceof OneOffExecutor) {
                 owner.remove((OneOffExecutor) this);
             }
    +        executableEstimatedDuration = DEFAULT_ESTIMATED_DURATION;
             queue.scheduleMaintenance();
         }
     
    @@ -686,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);
    @@ -716,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;
             }
         }
     
    @@ -776,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();
             }
    @@ -804,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;
    @@ -906,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);
             }
         }
     
    @@ -981,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/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 bdd99d70c2dbb6ce6ab8cee7efb312236584bc2a..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;
         }
    @@ -677,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
          */
    @@ -776,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.)
    @@ -791,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.)
    @@ -1317,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.
    @@ -1547,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..166a2cbcf2f20cf0469a77d602d15226e677e7fd 100644
    --- a/core/src/main/java/hudson/model/ModelObject.java
    +++ b/core/src/main/java/hudson/model/ModelObject.java
    @@ -27,7 +27,7 @@ package hudson.model;
      * 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
    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 ab1ca6ca66bc95077e20d6c3473bf7a71d283cfe..c61ee7abc138a0dc824f2ae1c76abf916d30e3a7 100644
    --- a/core/src/main/java/hudson/model/Node.java
    +++ b/core/src/main/java/hudson/model/Node.java
    @@ -62,6 +62,7 @@ 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;
    @@ -97,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.
    @@ -345,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());
    @@ -394,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()));
             }
    @@ -509,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..64fc925a8ec8a76cd27e1ba96056b97a5daa95eb 100644
    --- a/core/src/main/java/hudson/model/ParameterValue.java
    +++ b/core/src/main/java/hudson/model/ParameterValue.java
    @@ -56,12 +56,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>
    diff --git a/core/src/main/java/hudson/model/ParametersAction.java b/core/src/main/java/hudson/model/ParametersAction.java
    index 6f7ecca9f8d4c137e4d09b10ea903c3bb448a519..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();
             }
    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 f4e8a8eaaa71e1d8097ae16995b02158ab24cd5d..1eaeb089310d8be5faf4325af606a43eec149951 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,13 @@ 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.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 +80,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 +98,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 +106,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 +381,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 +459,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 +507,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 +760,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 +1112,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 +1182,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();
    +        }
    +
    +        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 !buildables.containsKey(t) && !pendings.containsKey(t);
    +
    +        return null;
         }
     
         /**
    @@ -1413,6 +1455,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 +1469,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 +1518,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 +1534,8 @@ public class Queue extends ResourceController implements Saveable {
                                 // determine if they are blocked by the lucky winner
                                 updateSnapshot();
                             }
    +                    } else {
    +                        p.setCauseOfBlockage(causeOfBlockage);
                         }
                     }
                 }
    @@ -1501,8 +1550,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 +1560,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 +1586,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
    @@ -1596,7 +1653,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
    @@ -1652,6 +1709,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,7 +1732,6 @@ public class Queue extends ResourceController implements Saveable {
                 ConsistentHash<Node> hash = new ConsistentHash<Node>(NODE_HASH);
                 hash.addAll(hashSource);
     
    -            Label lbl = p.getAssignedLabel();
                 String fullDisplayName = p.task.getFullDisplayName();
                 for (Node n : hash.list(fullDisplayName)) {
                     final Computer c = n.toComputer();
    @@ -1678,18 +1745,25 @@ public class Queue extends ResourceController implements Saveable {
                         continue;
                     }
     
    -                LOGGER.log(Level.FINEST, "Creating flyweight task {0} for computer {1}", new Object[]{fullDisplayName, 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();
    @@ -1775,18 +1849,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,
    @@ -1818,6 +1896,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.
    @@ -1926,7 +2016,7 @@ 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.
          */
         public interface Executable extends Runnable {
    @@ -2061,6 +2151,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);
    @@ -2126,8 +2217,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);
             }
     
             /**
    @@ -2193,7 +2286,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();
             }
     
    @@ -2454,29 +2549,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) {
    @@ -2577,7 +2680,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;
                 }
             }
     
    @@ -2943,4 +3046,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 84a31086a9502b02940931a2c32b9819c36dfdc9..6b9c3fbbd97d1e9a8dd1d8b91565fe6e17a73bbe 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,26 +1150,26 @@ 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;
                 }
             }
    @@ -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());
             
    @@ -1723,11 +1793,14 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
     
                         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);
    @@ -1938,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
    @@ -2138,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)) {
    @@ -2287,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;
         }
     
    @@ -2329,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;
    @@ -2480,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 4224f19cfcf0292a69754b665d2018b82f106243..f4395847929c2df9356113c0238708f70b9f4951 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}.
    @@ -225,6 +229,15 @@ public abstract class Slave extends Node implements Serializable {
         }
     
         public ComputerLauncher getLauncher() {
    +        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;
         }
    @@ -389,20 +402,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);
    @@ -442,19 +493,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);
    @@ -465,7 +516,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)) {
    @@ -478,7 +529,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();
         }
    @@ -502,14 +558,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(false)
    -                    : new CommandLauncher(agentCommand);
    -        }
             if(nodeProperties==null)
    -            nodeProperties = new DescribableList<NodeProperty<?>,NodePropertyDescriptor>(Jenkins.getInstance().getNodesObject());
    +            nodeProperties = new DescribableList<>(this);
             return this;
         }
     
    @@ -674,5 +724,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..49d2025ea72630f13abf381438ec6e18d45480a6 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;
    @@ -37,6 +38,7 @@ 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 +53,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;
    @@ -70,6 +71,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 +148,9 @@ 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 String UPDATE_CENTER_URL = SystemProperties.getString(UpdateCenter.class.getName()+".updateCenterUrl","https://updates.jenkins.io/");
     
         /**
          * Read timeout when downloading plugins, defaults to 1 minute
    @@ -292,7 +294,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
         }
     
         public Api getApi() {
    -        Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
             return new Api(this);
         }
     
    @@ -364,7 +365,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 +417,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 +466,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<>();
    @@ -576,7 +574,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.
    @@ -620,7 +618,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
          */
         @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 +636,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 +651,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 +721,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 +737,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 +975,6 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
             return results;
         }
     
    -
         /**
          * {@link AdministrativeMonitor} that checks if there's Jenkins update.
          */
    @@ -1107,12 +1099,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 +1130,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 +1157,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 +1175,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 +1218,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 +1245,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 +1279,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 +1401,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 +1595,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 +1633,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 +1832,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 +2063,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 +2199,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);
             }
         }
    @@ -2198,6 +2302,22 @@ 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.
          */
    diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java
    index ddf399ceca561bf518104071cd8a349c6b4bf115..c5a2cd260bee48a11741dbbec7a049c5adfe3f46 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;
    @@ -120,11 +121,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 +132,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 +222,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 +306,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 +479,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 +486,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 +528,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 +558,7 @@ public class UpdateSite {
                             }
                         }
                     }
    -                plugins.put(e.getKey(), p);
    +                plugins.put(Util.intern(e.getKey()), p);
                 }
     
                 connectionCheckUrl = (String)o.get("connectionCheckUrl");
    @@ -628,18 +618,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 +659,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 +739,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 +837,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 +927,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.
    @@ -952,13 +978,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 +992,31 @@ 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.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;
    @@ -1106,10 +1133,19 @@ public class UpdateSite {
             }
     
             public boolean isNeededDependenciesForNewerJenkins() {
    -            for (Plugin p: getNeededDependencies()) {
    -                if (p.isForNewerHudson() || p.isNeededDependenciesForNewerJenkins()) return true;
    -            }
    -            return false;
    +            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;
    +            });
             }
     
             /**
    @@ -1121,11 +1157,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 +1178,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 895729ade42f29a0933da2ca1ff6708667b7fdb1..654d41432fca3777a21521df3b166fdfe6160b61 100644
    --- a/core/src/main/java/hudson/model/User.java
    +++ b/core/src/main/java/hudson/model/User.java
    @@ -97,6 +97,9 @@ 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;
    @@ -104,6 +107,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.
      *
    @@ -129,7 +134,7 @@ import org.springframework.dao.DataAccessException;
      * @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 {
     
         /**
          * The username of the 'unknown' user used to avoid null user references.
    @@ -326,23 +331,70 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          * @since 1.419
          */
         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.getInstance().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", new Object[]{ id });
    +            return userDetails;
             } catch (UserMayOrMayNotExistException e) {
    +            LOGGER.log(Level.FINE, "The user {0} may or may not exist in the SecurityRealm, so we provide minimum access", new Object[]{ id });
                 // backend can't load information about other users. so use the stored information if available
             } 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", new Object[]{ id });
    +            }else{
    +                LOGGER.log(Level.FINE, "The user {0} was not found in the SecurityRealm", new Object[]{ 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() });
             }
    +        
    +        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}
    +            );
    +        }
    +    }
     
    -        // seems like a legitimate user we have no idea about. proceed with minimum access
    -        return new UsernamePasswordAuthenticationToken(id, "",
    -            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());
         }
     
         /**
    @@ -387,6 +439,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         /**
          * Gets the {@link User} object by its id or full name.
          *
    +     * 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.)
    @@ -401,23 +457,16 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          *      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) {
    +    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);
    +        // TODO: In many cases the method should receive the canonical ID.
    +        // Maybe it makes sense to try AllUsers.byName().get(idkey) before invoking all resolvers and other stuff
    +        // oleg-nenashev: FullNameResolver with User.getAll() loading and iteration makes me think it's a good idea.
     
    -        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;
    -            }
    -        }
    +        String id = CanonicalIdResolver.resolve(idOrFullName, context);
             // 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");
    @@ -432,6 +481,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          *      {@code create} is false.
          */
         private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create) {
    +        return getOrCreate(id, fullName, create, getUnsanitizedLegacyConfigFileFor(id));
    +    }
    +
    +    private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create, File unsanitizedLegacyConfigFile) {
             String idkey = idStrategy().keyFor(id);
     
             byNameLock.readLock().lock();
    @@ -442,46 +495,18 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
                 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);
    -                    }
    -                }
    -            }
    -        }
    -
    -        File unsanitizedLegacyConfigFile = getUnsanitizedLegacyConfigFileFor(id);
    -        if (unsanitizedLegacyConfigFile.exists() && !unsanitizedLegacyConfigFile.equals(configFile)) {
    +        boolean mustMigrateLegacyConfig = isMigrationRequiredForLegacyConfigFile(unsanitizedLegacyConfigFile, configFile);
    +        if (mustMigrateLegacyConfig) {
                 File ancestor = unsanitizedLegacyConfigFile.getParentFile();
                 if (!configFile.exists()) {
                     try {
                         Files.createDirectory(configFile.getParentFile().toPath());
                         Files.move(unsanitizedLegacyConfigFile.toPath(), configFile.toPath());
    +                    LOGGER.log(Level.INFO, "Migrated user record from {0} to {1}", new Object[] {unsanitizedLegacyConfigFile, configFile});
                     } catch (IOException | InvalidPathException e) {
                         LOGGER.log(
                                 Level.WARNING,
    -                            String.format("Failed to migrate user record from %s to %s, see SECURITY-499 for more information", idStrategy().legacyFilenameOf(id), idStrategy().filenameOf(id)),
    +                            String.format("Failed to migrate user record from %s to %s", unsanitizedLegacyConfigFile, configFile),
                                 e);
                     }
                 }
    @@ -533,15 +558,79 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             }
             return u;
         }
    +    
    +    private static boolean isMigrationRequiredForLegacyConfigFile(@Nonnull File legacyConfigFile, @Nonnull File newConfigFile){
    +        boolean mustMigrateLegacyConfig = legacyConfigFile.exists() && !legacyConfigFile.equals(newConfigFile);
    +        if(mustMigrateLegacyConfig){
    +            try{
    +                // TODO Could be replace by Util.isDescendant(getRootDir(), legacyConfigFile) in 2.80+
    +                String canonicalLegacy = legacyConfigFile.getCanonicalPath();
    +                String canonicalUserDir = getRootDir().getCanonicalPath();
    +                if(!canonicalLegacy.startsWith(canonicalUserDir + File.separator)){
    +                    // without that check, the application config.xml could be moved (i.e. erased from application PoV)
    +                    mustMigrateLegacyConfig = false;
    +                    LOGGER.log(Level.WARNING, String.format(
    +                            "Attempt to escape from users directory with %s, migration aborted, see SECURITY-897 for more information",
    +                            legacyConfigFile.getAbsolutePath()
    +                    ));
    +                }
    +            }
    +            catch (IOException e){
    +                mustMigrateLegacyConfig = false;
    +                LOGGER.log(
    +                        Level.WARNING,
    +                        String.format(
    +                                "Failed to determine the canonical path of %s, migration aborted, see SECURITY-897 for more information", 
    +                                legacyConfigFile.getAbsolutePath()
    +                        ),
    +                        e
    +                );
    +            }
    +        }
    +        return mustMigrateLegacyConfig;
    +    }
     
         /**
          * Gets the {@link User} object by its id or full name.
    +     *
    +     * 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)}.
          */
    +    @Deprecated
         public static @Nonnull User get(String idOrFullName) {
    -        return get(idOrFullName,true);
    +        return getOrCreateByIdOrFullName(idOrFullName);
         }
     
    +    /**
    +     * Get the user by ID or Full Name.
    +     *
    +     * 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.
    @@ -578,6 +667,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
          *            <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.
    +     * @since 1.651.2 / 2.3
          */
         public static @Nullable User getById(String id, boolean create) {
             return getOrCreate(id, id, create);
    @@ -711,6 +801,16 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         public @Override String toString() {
             return fullName;
         }
    +    
    +    /**
    +     * Returns the folder that store all the user information
    +     * Useful for plugins to save a user-specific file aside the config.xml
    +     * 
    +     * @since 2.129
    +     */
    +    public File getUserFolder(){
    +        return getUserFolderFor(this.id);
    +    }
     
         /**
          * The file we save our configuration.
    @@ -720,17 +820,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         }
     
         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 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 static File getUserFolderFor(String id){
    +        return new File(getRootDir(), idStrategy().filenameOf(id));
         }
     
         private static File getUnsanitizedLegacyConfigFileFor(String id) {
    @@ -785,6 +879,19 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             SaveableListener.fireOnChange(this, getConfigFile());
         }
     
    +    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);
    +        }
    +    }
    +
         /**
          * Deletes the data directory and removes this user from Hudson.
          *
    @@ -921,22 +1028,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         }
     
         public ACL getACL() {
    -        final ACL base = Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
    +        ACL base = Jenkins.getInstance().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));
         }
     
         /**
    @@ -980,10 +1075,6 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
             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))
    @@ -1027,6 +1118,24 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
         public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
             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;
    +    }
    +
    +    /**
    +     * 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");
    +
         
         /**
          * Gets list of Illegal usernames, for which users should not be created.
    @@ -1052,9 +1161,13 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
                 File[] subdirs = getRootDir().listFiles((FileFilter) DirectoryFileFilter.INSTANCE);
                 if (subdirs != null) {
                     for (File subdir : subdirs) {
    -                    if (new File(subdir, "config.xml").exists()) {
    +                    if (subdir.equals(getRootDir())) {
    +                        continue; // ignore the parent directory in case of stray config.xml
    +                    }
    +                    File configFile = new File(subdir, "config.xml");
    +                    if (configFile.exists()) {
                             String name = strategy.idFromFilename(subdir.getName());
    -                        getOrCreate(name, /* <init> calls load(), probably clobbering this anyway */name, true);
    +                        getOrCreate(name, /* <init> calls load(), probably clobbering this anyway */name, true, configFile);
                         }
                     }
                 }
    @@ -1071,15 +1184,21 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
              */
             @GuardedBy("User.byNameLock")
             static ConcurrentMap<String,User> byName() {
    -            ExtensionList<AllUsers> instances = ExtensionList.lookup(AllUsers.class);
    -            if (instances.size() != 1) {
    -                throw new IllegalStateException();
    -            }
    -            return instances.get(0).byName;
    +            return ExtensionList.lookupSingleton(AllUsers.class).byName;
             }
     
         }
     
    +    /**
    +     * Resolves User IDs by ID, full names or other strings.
    +     *
    +     * 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)}.
    +     *
    +     * @since 1.479
    +     * @see jenkins.model.DefaultUserCanonicalIdResolver
    +     * @see FullNameIdResolver
    +     */
         public static abstract class CanonicalIdResolver extends AbstractDescribableImpl<CanonicalIdResolver> implements ExtensionPoint, Comparable<CanonicalIdResolver> {
     
             /**
    @@ -1089,6 +1208,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
              */
             public static final String REALM = "realm";
     
    +        @Override
             public int compareTo(CanonicalIdResolver o) {
                 // reverse priority order
                 int i = getPriority();
    @@ -1100,12 +1220,56 @@ public class User extends AbstractModelObject implements AccessControlled, Descr
              * 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, @Nonnull Map<String, ?> context);
     
    +        /**
    +         * Gets priority of the resolver.
    +         * Higher priority means that it will be checked earlier.
    +         *
    +         * 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;
             }
     
    +        //TODO: It is too late to use Extension Point ordinals, right?
    +        //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()) {
    +                //TODO: add try/catch for Runtime exceptions? It should not happen now && it may cause performance degradation
    +                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;
    +        }
         }
     
     
    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 f19ab9a6058d53cbe4c421a56269194120c2a364..fcab16b90b2043c855c23b0c4accc7ad3f7c071f 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;
    @@ -114,9 +113,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 +133,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 +171,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 +189,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 +453,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 +547,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 +600,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) {}
    @@ -977,7 +990,6 @@ public abstract class View extends AbstractModelObject implements AccessControll
             rename(req.getParameter("name"));
     
             getProperties().rebuild(req, req.getSubmittedForm(), getApplicablePropertyDescriptors());
    -        updateTransientActions();  
     
             save();
     
    @@ -1053,11 +1065,11 @@ 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");
    -        getOwner().checkPermission(Item.CREATE);
             Categories categories = new Categories();
             int order = 0;
             JellyContext ctx;
    @@ -1138,7 +1150,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 {
    @@ -1196,7 +1208,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
    @@ -1205,6 +1218,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);
             }
    @@ -1349,7 +1363,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/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/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/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 d7241e9eff619d13af4c0746e8830854f5b83b4c..9f77f9bb779e3ed08e0ddc750364fa4a2d5bc817 100644
    --- a/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java
    +++ b/core/src/main/java/hudson/node_monitors/ResponseTimeMonitor.java
    @@ -46,20 +46,24 @@ 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) {
    -            if (c.getChannel() == null) {
    -                return null;
    -            }
                 return new Step1(get(c));
             }
     
             @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));
    @@ -74,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 14c67b90343201741e8ccbfb726191123119f618..120b8bc2a16bda6ef51a40fa0fec9ab96678d98e 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) {
    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 c33810a7f8f41964110983f4cfcbcc4b2f230e90..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;
    +            }
             }
         }
     
    @@ -563,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>
    @@ -669,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..6b07b99a5d615933f7a3a3a9c44010463c1edd8f 100644
    --- a/core/src/main/java/hudson/search/Search.java
    +++ b/core/src/main/java/hudson/search/Search.java
    @@ -42,10 +42,13 @@ import java.util.logging.Logger;
     
     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 +66,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 +143,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) {
    @@ -323,16 +326,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 +407,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/test/src/test/java/hudson/slaves/ComputerConnectorTest.java b/core/src/main/java/hudson/security/AccountCreationFailedException.java
    similarity index 70%
    rename from test/src/test/java/hudson/slaves/ComputerConnectorTest.java
    rename to core/src/main/java/hudson/security/AccountCreationFailedException.java
    index 9112f4f10d63035cf0d89a472a9a99cd14597688..bab5345db1647133d7604a1b48ca0863d74437a0 100644
    --- a/test/src/test/java/hudson/slaves/ComputerConnectorTest.java
    +++ b/core/src/main/java/hudson/security/AccountCreationFailedException.java
    @@ -1,7 +1,7 @@
     /*
      * The MIT License
      *
    - * Copyright (c) 2010, InfraDNA, 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 hudson.slaves;
     
    -import org.jvnet.hudson.test.HudsonTestCase;
    +package hudson.security;
    +
    +import org.kohsuke.accmod.Restricted;
    +import org.kohsuke.accmod.restrictions.NoExternalUse;
     
     /**
    - * @author Kohsuke Kawaguchi
    + * Thrown if an account creation was attempted but failed due to invalid data being entered into a form.
    + *
    + * @author Philipp Nowak
      */
    -public class ComputerConnectorTest extends HudsonTestCase {
    -    public void testConfigRoundtrip() throws Exception {
    -        CommandConnector cc = new CommandConnector("abc def");
    -        assertEqualDataBoundBeans(cc,configRoundtrip(cc));
    +@Restricted(NoExternalUse.class)
    +public class AccountCreationFailedException extends Exception {
    +    public AccountCreationFailedException(String message) {
    +        super(message);
         }
     }
    diff --git a/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java b/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
    index bda002cf106fd51e1c09345ae1b3f160759e3466..1199e303ef9d54d41f9a98413880d855187e0d09 100644
    --- a/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
    +++ b/core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java
    @@ -39,7 +39,7 @@ 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
    diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java
    index a875908f212d7ec472f589dad9a27f93090e9260..a273e984b201c96d3c33c86666bb04ac297354e1 100644
    --- a/core/src/main/java/hudson/security/AuthorizationStrategy.java
    +++ b/core/src/main/java/hudson/security/AuthorizationStrategy.java
    @@ -95,9 +95,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 +104,7 @@ public abstract class AuthorizationStrategy extends AbstractDescribableImpl<Auth
                     }
     
                     return hasPermission;
    -            }
    -        };
    +        });
         }
         
         /**
    @@ -225,12 +222,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/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/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
    index b82e8b70d0e31915bb58695d106cb7401e8ba637..83d31bd1e1c3fab546773cea9507eb7cfc96ceea 100644
    --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
    +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java
    @@ -41,6 +41,7 @@ import hudson.util.PluginServletFilter;
     import hudson.util.Protector;
     import hudson.util.Scrambler;
     import hudson.util.XStream2;
    +import jenkins.security.SecurityListener;
     import net.sf.json.JSONObject;
     import org.acegisecurity.Authentication;
     import org.acegisecurity.AuthenticationException;
    @@ -64,6 +65,7 @@ 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;
    @@ -72,15 +74,13 @@ 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 org.kohsuke.accmod.Restricted;
     import org.kohsuke.accmod.restrictions.NoExternalUse;
    @@ -95,6 +95,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>
    @@ -253,11 +262,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);
         }
    @@ -286,6 +304,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.
          *
    @@ -318,65 +365,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} set to a non-null value 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);
                 }
    @@ -384,7 +475,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 {
    @@ -442,8 +541,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
    @@ -457,6 +557,8 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea
              */
             public String errorMessage;
     
    +        public HashMap<String, String> errors = new HashMap<String, String>();
    +
             public SignupInfo() {
             }
     
    @@ -638,7 +740,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
    @@ -647,11 +749,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;
    @@ -687,11 +789,11 @@ 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 {
    +        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);
             }
         };
    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 6cb69906e095160ba64cdc2fe558981acb0f6053..90d60b7deb83ab1daacacfff9243bdc02be89654 100644
    --- a/core/src/main/java/hudson/security/SecurityRealm.java
    +++ b/core/src/main/java/hudson/security/SecurityRealm.java
    @@ -75,7 +75,7 @@ import java.util.logging.Logger;
      *
      * <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}.
      *
    @@ -143,7 +143,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>
    @@ -205,8 +205,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() {
    @@ -225,7 +225,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>
    @@ -304,6 +304,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);
     
    @@ -312,12 +315,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"/>
    @@ -402,7 +405,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..ff0f374ca4c897796f00853f3b0b1be49baffd58 100644
    --- a/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java
    +++ b/core/src/main/java/hudson/security/TokenBasedRememberMeServices2.java
    @@ -123,19 +123,39 @@ public class TokenBasedRememberMeServices2 extends TokenBasedRememberMeServices
     
         @Override
         public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    -        try {
    -            return super.autoLogin(request, response);
    -        } catch (Exception e) {
    -            cancelCookie(request, response, "Failed to handle remember-me cookie: "+Functions.printThrowable(e));
    +        if(Jenkins.getInstance().isDisableRememberMe()){
    +            cancelCookie(request, response, null);
                 return null;
    +        }else {
    +            try {
    +                return super.autoLogin(request, response);
    +            } catch (Exception e) {
    +                cancelCookie(request, response, "Failed to handle remember-me cookie: " + Functions.printThrowable(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 +168,10 @@ 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());
    +    }
     
    -	/**
    +    /**
          * 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..1f3c68c743f91c1f56d75ad41befd48be996abf6 100644
    --- a/core/src/main/java/hudson/security/csrf/CrumbIssuer.java
    +++ b/core/src/main/java/hudson/security/csrf/CrumbIssuer.java
    @@ -193,6 +193,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/ChannelPinger.java b/core/src/main/java/hudson/slaves/ChannelPinger.java
    index 14ac43dc35ac8500775065985dd50f36c3e4bbd0..eac834a76fa437933d8090fa1b460800f51df9e2 100644
    --- a/core/src/main/java/hudson/slaves/ChannelPinger.java
    +++ b/core/src/main/java/hudson/slaves/ChannelPinger.java
    @@ -34,6 +34,8 @@ 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;
    @@ -119,21 +121,23 @@ public class ChannelPinger extends ComputerListener {
         }
     
         @VisibleForTesting
    -    /*package*/ static class SetUpRemotePing extends MasterToSlaveCallable<Void, IOException> {
    +    @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(), null, pingTimeoutSeconds, pingIntervalSeconds, false);
    +            // No sense in setting up channel pinger if the channel is being closed
    +            setUpPingForChannel(getOpenChannelOrFail(), null, pingTimeoutSeconds, pingIntervalSeconds, false);
                 return null;
             }
     
    @@ -176,7 +180,8 @@ public class ChannelPinger extends ComputerListener {
         }
     
         @VisibleForTesting
    -    /*package*/ static void setUpPingForChannel(final Channel channel, final SlaveComputer computer, int timeoutSeconds, int intervalSeconds, final boolean analysis) {
    +    @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) {
    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 6f074da3b8aafbf593ddd6ae1718955c326de8ff..d9cc5f2039ea4c8f690f04e5f0d06972516da231 100644
    --- a/core/src/main/java/hudson/slaves/Cloud.java
    +++ b/core/src/main/java/hudson/slaves/Cloud.java
    @@ -67,7 +67,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 +80,9 @@ import java.util.concurrent.Future;
      *
      * <h3>Views</h3>
      *
    - * Since version 2.64, 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
    @@ -128,14 +128,6 @@ public abstract class Cloud extends Actionable implements ExtensionPoint, Descri
             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);
    -    }
    -
         /**
          * Provisions new {@link Node}s from this cloud.
          *
    diff --git a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java
    index db2b80cf455abbeb2fa939776ed5f418268fa805..1f898fcf74dbc2f4cc8e9eaeeddc360b845a3390 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.*;
     
    diff --git a/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java
    index accfca80c3a4242756984095d9d6961fb1766617..1955f6cc0de065434f8b98003eeeb28d52cf5774 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;
    @@ -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/CommandConnector.java b/core/src/main/java/hudson/slaves/CommandConnector.java
    deleted file mode 100644
    index be234639860036db307959f9d3f9551407aff0c8..0000000000000000000000000000000000000000
    --- a/core/src/main/java/hudson/slaves/CommandConnector.java
    +++ /dev/null
    @@ -1,81 +0,0 @@
    -/*
    - * The MIT License
    - *
    - * Copyright (c) 2010, InfraDNA, 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.slaves;
    -
    -import hudson.EnvVars;
    -import hudson.Extension;
    -import hudson.Util;
    -import hudson.model.TaskListener;
    -import hudson.util.FormValidation;
    -import org.jenkinsci.Symbol;
    -import org.kohsuke.stapler.DataBoundConstructor;
    -
    -import java.io.IOException;
    -import jenkins.model.Jenkins;
    -import org.kohsuke.stapler.QueryParameter;
    -
    -/**
    - * Executes a program on the master and expect that script to connect.
    - *
    - * @author Kohsuke Kawaguchi
    - */
    -public class CommandConnector extends ComputerConnector {
    -    public final String command;
    -
    -    @DataBoundConstructor
    -    public CommandConnector(String command) {
    -        this.command = command;
    -        Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
    -    }
    -
    -    private Object readResolve() {
    -        Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
    -        return this;
    -    }
    -
    -    @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 FormValidation doCheckCommand(@QueryParameter String value) {
    -            if (!Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) {
    -                return FormValidation.warning(Messages.CommandLauncher_cannot_be_configured_by_non_administrato());
    -            }
    -            if (Util.fixEmptyAndTrim(value) == null) {
    -                return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand());
    -            } else {
    -                return FormValidation.ok();
    -            }
    -        }
    -
    -    }
    -}
    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 af2158bdab315f0572e9b3ac05a3704827d1515f..0000000000000000000000000000000000000000
    --- a/core/src/main/java/hudson/slaves/CommandLauncher.java
    +++ /dev/null
    @@ -1,211 +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.security.ACL;
    -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;
    -        Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
    -    }
    -    
    -    private Object readResolve() {
    -        Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
    -        return this;
    -    }
    -
    -    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 (!Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) {
    -                return FormValidation.warning(Messages.CommandLauncher_cannot_be_configured_by_non_administrato());
    -            }
    -            if(Util.fixEmptyAndTrim(value)==null)
    -                return FormValidation.error(Messages.CommandLauncher_NoLaunchCommand());
    -            else
    -                return FormValidation.ok();
    -        }
    -    }
    -}
    diff --git a/core/src/main/java/hudson/slaves/ComputerLauncher.java b/core/src/main/java/hudson/slaves/ComputerLauncher.java
    index 9691408eeb3f1df9d9987379b6202706ec5ea9d2..938660e4f6de30e01c7627360809226020943736 100644
    --- a/core/src/main/java/hudson/slaves/ComputerLauncher.java
    +++ b/core/src/main/java/hudson/slaves/ComputerLauncher.java
    @@ -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/ConnectionActivityMonitor.java b/core/src/main/java/hudson/slaves/ConnectionActivityMonitor.java
    index 4bd542d7de3d1e2c77e3024cce7f862c7d590eb0..dc3ac88ac2517a01f0e0031eab17007dbc201b8e 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;
    @@ -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/DumbSlave.java b/core/src/main/java/hudson/slaves/DumbSlave.java
    index 835cf004ad23d02fb8fc8de08468f2122084a295..8142eb358facee47f8368640b03587fb1e710725 100644
    --- a/core/src/main/java/hudson/slaves/DumbSlave.java
    +++ b/core/src/main/java/hudson/slaves/DumbSlave.java
    @@ -65,8 +65,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 d0852590c33dae440dbdf7d5e1861dab97afd4bb..bed08613d8146fa87a5300833e05810a41c52c3a 100644
    --- a/core/src/main/java/hudson/slaves/JNLPLauncher.java
    +++ b/core/src/main/java/hudson/slaves/JNLPLauncher.java
    @@ -31,12 +31,14 @@ 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 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.
    @@ -67,21 +69,29 @@ public class JNLPLauncher extends ComputerLauncher {
         public final String vmargs;
     
         @Nonnull
    -    private RemotingWorkDirSettings workDirSettings;
    +    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(@CheckForNull String tunnel, @CheckForNull String vmargs, @Nonnull RemotingWorkDirSettings workDirSettings) {
    +    public JNLPLauncher(@CheckForNull String tunnel, @CheckForNull String vmargs) {
             this.tunnel = Util.fixEmptyAndTrim(tunnel);
             this.vmargs = Util.fixEmptyAndTrim(vmargs);
    -        this.workDirSettings = workDirSettings;
    -    }
    -    
    -    @Deprecated
    -    public JNLPLauncher(String tunnel, String vmargs) {
    -        // TODO: Enable workDir by default in API? Otherwise classes inheriting from JNLPLauncher
    -        // will need to enable the feature by default as well.
    -        // https://github.com/search?q=org%3Ajenkinsci+%22extends+JNLPLauncher%22&type=Code
    -        this(tunnel, vmargs, RemotingWorkDirSettings.getDisabledDefaults());
         }
     
         /**
    @@ -115,12 +125,17 @@ public class JNLPLauncher extends ComputerLauncher {
         /**
          * Returns work directory settings.
          * 
    -     * @since TODO
    +     * @since 2.72
          */
         @Nonnull
         public RemotingWorkDirSettings getWorkDirSettings() {
             return workDirSettings;
         }
    +
    +    @DataBoundSetter
    +    public final void setWorkDirSettings(@Nonnull RemotingWorkDirSettings workDirSettings) {
    +        this.workDirSettings = workDirSettings;
    +    }
         
         @Override
         public boolean isLaunchSupported() {
    @@ -172,7 +187,7 @@ public class JNLPLauncher extends ComputerLauncher {
              * 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 TODO
    +         * @since 2.73
              */
             public boolean isWorkDirSupported() {
                 // This property is included only for JNLPLauncher by default. 
    diff --git a/core/src/main/java/hudson/slaves/NodeDescriptor.java b/core/src/main/java/hudson/slaves/NodeDescriptor.java
    index 6f06289c38d35390d9a9a099b66dabb63965eca2..5262ece687087f14269d7a3a6ccea5602dc6cf37 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>
      *
    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 0e33a9dc6c4ed93ac349c1278fb993baf85623cd..9eb95290c7e9d7175fb4ab1efff0909745a45dae 100644
    --- a/core/src/main/java/hudson/slaves/RetentionStrategy.java
    +++ b/core/src/main/java/hudson/slaves/RetentionStrategy.java
    @@ -97,12 +97,7 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
          * @since 1.275
          */
         public void start(final @Nonnull T c) {
    -        Queue.withLock(new Runnable() {
    -            @Override
    -            public void run() {
    -                check(c);
    -            }
    -        });
    +        Queue.withLock((Runnable) () -> check(c));
         }
     
         /**
    @@ -123,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.
    diff --git a/core/src/main/java/hudson/slaves/SlaveComputer.java b/core/src/main/java/hudson/slaves/SlaveComputer.java
    index eaef01609de77434d69a4ba4176b034103e72e38..950c6b68aa90ad2a68f5f885ac4d40b0e45432f4 100644
    --- a/core/src/main/java/hudson/slaves/SlaveComputer.java
    +++ b/core/src/main/java/hudson/slaves/SlaveComputer.java
    @@ -38,15 +38,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,19 +56,25 @@ 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;
    @@ -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;
     
     
     /**
    @@ -377,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);
         }
     
    @@ -385,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.
    @@ -400,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);
    @@ -413,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
    @@ -470,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) {
    @@ -477,6 +549,9 @@ 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();
             }
         }
    @@ -494,25 +569,32 @@ public class SlaveComputer extends Computer {
             }
             @Override public Long call() {
                 Channel c = Channel.current();
    +            if (c == null) {
    +                return Long.valueOf(-1);
    +            }
                 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
    @@ -538,7 +620,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());
    @@ -654,6 +742,8 @@ public class SlaveComputer extends Computer {
     
         @RequirePOST
         public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
    +        checkPermission(CONNECT);
    +            
             if(channel!=null) {
                 req.getView(this,"already-launched.jelly").forward(req, rsp);
                 return;
    @@ -861,7 +951,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 slave side.
    +            } catch (ChannelClosedException e) {
    +                throw new IllegalStateException(e);
    +            }
     
                 return null;
             }
    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/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 6b4eaa98a305497387288d193b4eca6d35180358..0000000000000000000000000000000000000000
    --- a/core/src/main/java/hudson/tools/JDKInstaller.java
    +++ /dev/null
    @@ -1,962 +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.URI;
    -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")) {
    -                    /* Login flow:
    -                     * 1. /oaam_server/oamLoginPage.jsp: Form for username + password. Submit action is:
    -                     * 2. /oaam_server/login.do: Returns a 302 to:
    -                     * 3. /oaam_server/loginAuth.do: After 2 seconds, JS sets window.location to:
    -                     * 4. /oaam_server/authJump.do: Contains a single form with hidden inputs and JS that submits the form to:
    -                     * 5. /oam/server/dap/cred_submit: Returns a 302 to:
    -                     * 6. https://edelivery.oracle.com/osso_login_success: Returns a 302 to the download.
    -                     */
    -                    if (m.getURI().getPath().contains("/loginAuth.do")) { // You are redirected to this page immediately after logging in.
    -                        try {
    -                            Thread.sleep(2000); // Oracle website waits 2 seconds after logging in before redirecting.
    -                            m.releaseConnection();
    -                            m = new GetMethod(new URI(m.getURI(), "/oaam_server/authJump.do?jump=false", true).toString());
    -                            continue;
    -                        } catch (InterruptedException x) {
    -                            throw new IOException("Interrupted while logging in", x);
    -                        }
    -                    }
    -
    -                    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());
    -
    -                    if (m.getURI().getPath().contains("/authJump.do")) {
    -                        m = post;
    -                        continue;
    -                    }
    -
    -                    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("userid"))
    -                            v = u;
    -                        if (n.equals("pass")) {
    -                            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 fb488b247130c5ac37921b4f2192b886ed410aa6..acc86c16d3804ac92d33a391cabb9d7d7a87028a 100644
    --- a/core/src/main/java/hudson/util/ArgumentListBuilder.java
    +++ b/core/src/main/java/hudson/util/ArgumentListBuilder.java
    @@ -135,7 +135,7 @@ public class ArgumentListBuilder implements Serializable, Cloneable {
         }
         
         /**
    -     * @since TODO 
    +     * @since 2.72
          */
         public ArgumentListBuilder add(@Nonnull Iterable<String> args) {
             for (String arg : args) {
    @@ -165,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/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/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..e8a24b51b0b3927a40a9914a67df623d89f1aff1 100644 --- a/core/src/main/java/hudson/util/ProcessTree.java +++ b/core/src/main/java/hudson/util/ProcessTree.java @@ -43,7 +43,16 @@ import jenkins.security.SlaveToMasterCallable; 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 +65,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 +74,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 +103,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 +153,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 +178,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 +241,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 +264,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 +335,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 +394,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 +405,33 @@ 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("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 +439,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 +513,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 +526,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 +624,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 +688,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 +707,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 +755,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 +794,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 +822,103 @@ 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 (isPostJava8()) { // Java 9+ + 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()); + } + } + } + + //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; } } - private static boolean isPreJava8() { - int javaVersionAsAnInteger = Integer.parseInt(System.getProperty("java.version").replaceAll("\\.", "").replaceAll("_", "").substring(0, 2)); - return javaVersionAsAnInteger < 18; + // Java 9 uses new version format + private static boolean isPostJava8() { + String javaVersion = System.getProperty("java.version"); + return !javaVersion.startsWith("1."); } } static class Linux extends ProcfsUnix { + public Linux(boolean vetoersExist) { + super(vetoersExist); + } + protected LinuxProcess createProcess(int pid) throws IOException { return new LinuxProcess(pid); } @@ -825,7 +1007,7 @@ public abstract class ProcessTree implements Iterable, IProcessTree, } /** - * Implementation for Solaris that uses /proc. + * 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 +1033,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 +1277,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 +1291,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 +1362,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 +1407,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 +1498,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 +1513,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/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/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..149e82ba1d799c64aed358b1467ef09b587b1f7b 100644 --- a/core/src/main/java/hudson/util/io/ParserConfigurator.java +++ b/core/src/main/java/hudson/util/io/ParserConfigurator.java @@ -78,17 +78,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 501a005a5dcafe38feea801e3f4b4fa3829ade52..4d4eb4f2be2f0af8cbb495fbca9ebc6fe967348a 100644 --- a/core/src/main/java/jenkins/FilePathFilter.java +++ b/core/src/main/java/jenkins/FilePathFilter.java @@ -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/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 18a39dedb7c0a10eb25f10f5e6a9b159785fea9c..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. * + * 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/SlaveToMasterFileCallable.java b/core/src/main/java/jenkins/SlaveToMasterFileCallable.java index 10071cf1f88562acb0ec25ed2ba35a5ee16caef7..236bd738f2cc0c78ed68af9d3c3f91093ab343be 100644 --- a/core/src/main/java/jenkins/SlaveToMasterFileCallable.java +++ b/core/src/main/java/jenkins/SlaveToMasterFileCallable.java @@ -6,7 +6,7 @@ import org.jenkinsci.remoting.RoleChecker; /** * {@link FileCallable}s that can be executed on the master, sent by the agent. - * + * 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 { 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/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..df091a08eabc3e2c2e3c6ccc0d50e91f497574cf 100644 --- a/core/src/main/java/jenkins/install/InstallState.java +++ b/core/src/main/java/jenkins/install/InstallState.java @@ -32,6 +32,7 @@ import hudson.ExtensionPoint; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; import org.apache.commons.lang.StringUtils; /** * Jenkins install state. @@ -45,11 +46,42 @@ import org.apache.commons.lang.StringUtils; * @author tom.fennelly@gmail.com */ 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 +93,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 +107,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 +149,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 +200,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 +223,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 +240,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..3ae5a0ea24b96a4acf29e32ad81e6eedacee2bef 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; @@ -20,7 +22,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import jenkins.model.JenkinsLocationConfiguration; import jenkins.util.SystemProperties; +import jenkins.util.UrlHelper; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; @@ -28,6 +32,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 +44,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 +58,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 +82,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 +93,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 +133,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 +165,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 +174,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 +215,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,53 +239,104 @@ 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 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(); + + 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); + 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 { FileUtils.writeStringToFile(getUpdateStateFile(), v.toString()); @@ -279,7 +348,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 +377,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 +401,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 +427,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 +473,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(); @@ -461,7 +530,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 +543,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 +565,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 +605,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 +622,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/test/src/test/groovy/hudson/GroovyTest.groovy b/core/src/main/java/jenkins/model/DefaultSimplePageDecorator.java similarity index 72% rename from test/src/test/groovy/hudson/GroovyTest.groovy rename to core/src/main/java/jenkins/model/DefaultSimplePageDecorator.java index e512cf56ef9567647722cb0768bc408556e77c21..6a4a9278a389132cc05bb85cca80893965d8db51 100644 --- a/test/src/test/groovy/hudson/GroovyTest.groovy +++ b/core/src/main/java/jenkins/model/DefaultSimplePageDecorator.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2004-2009, Sun Microsystems, Inc. + * 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 @@ -21,25 +21,16 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson +package jenkins.model; -import org.junit.Rule -import org.junit.Test -import org.jvnet.hudson.test.JenkinsRule +import hudson.Extension; /** - * First groovy test! + * In case there are no other implementations we will fallback to this implementation. * - * @author Kohsuke Kawaguchi + * 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" */ -public class GroovyTest { - - @Rule - public JenkinsRule j = new JenkinsRule() - - @Test - void test() { - def wc = j.createWebClient(); - wc.goTo(""); - } +@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..76320768b0d5cd1f1b2f92f9f0de0f25d27a578e 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. @@ -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 a9acf97420e38378a4cc68f93067b19e36d84b28..f90e892e0b4a6cbbffadf4d6bdd1956510ed7c3f 100644 --- a/core/src/main/java/jenkins/model/IdStrategy.java +++ b/core/src/main/java/jenkins/model/IdStrategy.java @@ -303,7 +303,11 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp } else { break; } - buf.append(Character.valueOf((char)Integer.parseInt(hex.toString(), 16))); + try { + buf.append(Character.valueOf((char)Integer.parseInt(hex.toString(), 16))); + } catch (NumberFormatException x) { + buf.append('$').append(hex); + } } } return buf.toString(); @@ -509,7 +513,11 @@ public abstract class IdStrategy extends AbstractDescribableImpl imp } else { break; } - buf.append(Character.valueOf((char)Integer.parseInt(hex.toString(), 16))); + try { + buf.append(Character.valueOf((char)Integer.parseInt(hex.toString(), 16))); + } catch (NumberFormatException x) { + buf.append('$').append(hex); + } } } return buf.toString(); 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 99c5d25e941ad141a7262d53d0b3ce1cdfbe3a62..001e649987b9b9e47c96dd0f95ef269228a3cd3c 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,7 @@ import hudson.*; import hudson.Launcher.LocalLauncher; import jenkins.AgentProtocol; import jenkins.diagnostics.URICheckEncodingMonitor; +import jenkins.security.RedactSecretJsonInErrorMessageSanitizer; import jenkins.util.SystemProperties; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; @@ -106,7 +108,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 +159,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 +180,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 +250,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 +285,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 +292,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 +329,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 +404,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. @@ -462,20 +463,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 +743,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; @@ -855,7 +851,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 +894,14 @@ 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)); + adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+SESSION_HASH, TimeUnit.DAYS.toMillis(365)); - // 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); - } - } + ClassFilterImpl.register(); // initialization consists of ... executeReactor( is, @@ -947,9 +925,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 +944,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(); @@ -1042,25 +1018,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(); } /** @@ -1581,7 +1560,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. @@ -1751,42 +1730,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. @@ -2199,46 +2142,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); @@ -2456,12 +2359,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, @@ -2498,11 +2416,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); + } } /** @@ -2627,7 +2547,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * * @since 1.433 */ - public Injector getInjector() { + public @CheckForNull Injector getInjector() { return lookup(Injector.class); } @@ -2643,7 +2563,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)); } /** @@ -2663,8 +2584,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)); } /** @@ -2765,7 +2686,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(); @@ -3077,6 +3007,87 @@ 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); + if (newBuildsDir != null && !buildsDir.equals(newBuildsDir)) { + + checkRawBuildsDir(newBuildsDir); + LOGGER.log(Level.WARNING, "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)) { + LOGGER.log(Level.WARNING, "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 { @@ -3106,8 +3117,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 @@ -3117,10 +3129,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 @@ -3135,7 +3147,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(); @@ -3187,6 +3199,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(); } }); @@ -3198,6 +3213,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()); } @@ -3285,6 +3308,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (JenkinsJVM.isJenkinsJVM()) { JenkinsJVMAccess._setJenkinsJVM(oldJenkinsJVM); } + ClassFilterImpl.unregister(); } } @@ -3667,17 +3691,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) @@ -3726,9 +3745,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 { @@ -4182,7 +4199,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(); @@ -4219,7 +4236,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(); @@ -4266,29 +4283,18 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RequirePOST public void doExit( StaplerRequest req, StaplerResponse rsp ) throws IOException { checkPermission(ADMINISTER); - new Thread("exit thread") { - @Override - public void run() { - try { - ACL.impersonate(ACL.SYSTEM); - 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"); - try (PrintWriter w = rsp.getWriter()) { - w.println("Shutting down"); - } - } - - cleanUp(); - System.exit(0); - - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to shut down Jenkins", e); - } + LOGGER.info(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"); + try (PrintWriter w = rsp.getWriter()) { + w.println("Shutting down"); } - }.start(); + } + + cleanUp(); + System.exit(0); } @@ -4308,7 +4314,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); @@ -4548,7 +4554,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); } /** @@ -4558,7 +4564,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(); } /** @@ -4615,7 +4621,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(); @@ -4942,7 +4948,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(); @@ -5027,7 +5033,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 } @@ -5096,6 +5102,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 b7390ad3d3ee4baf5731090dd2b2bb1965de49df..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,18 +44,20 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { // just to suppress warnings private transient String charset,useSsl; + public static @Nonnull JenkinsLocationConfiguration get() { + return GlobalConfiguration.all().getInstance(JenkinsLocationConfiguration.class); + } + /** - * Gets local configuration. - * - * @return {@code null} if the {@link GlobalConfiguration#all()} list does not contain this extension. - * Most likely it means that the Jenkins instance has not been fully loaded yet. + * Gets local configuration. For explanation when it could die, see {@link #get()} */ - public static @CheckForNull JenkinsLocationConfiguration get() { - return GlobalConfiguration.all().get(JenkinsLocationConfiguration.class); - } - - public JenkinsLocationConfiguration() { - load(); + @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 @@ -63,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); @@ -125,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"); @@ -152,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..4d3b0682bda322cf6770912fce140a8aa930fccd --- /dev/null +++ b/core/src/main/java/jenkins/model/SimplePageDecorator.java @@ -0,0 +1,72 @@ +/* + * 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.DescriptorExtensionList; +import hudson.ExtensionPoint; +import hudson.model.Describable; +import hudson.model.Descriptor; +/** + * 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(); + } + + /** + * The first found LoginDecarator, there can only be one. + * @return the first found {@link SimplePageDecorator} + */ + public static SimplePageDecorator first(){ + DescriptorExtensionList descriptorList = Jenkins.getInstanceOrNull().getDescriptorList(SimplePageDecorator.class); + if (descriptorList.size() >= 1) { + return descriptorList.get(0); + } else { + return null; + } + } + +} 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/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 dce404243ac25a76439210cd027d95739cf33421..1523b5a2ad8a63258bb3bacf0c1d8302b778b67a 100644 --- a/core/src/main/java/jenkins/scm/RunWithSCM.java +++ b/core/src/main/java/jenkins/scm/RunWithSCM.java @@ -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/jenkins/security/ApiCrumbExclusion.java b/core/src/main/java/jenkins/security/ApiCrumbExclusion.java new file mode 100644 index 0000000000000000000000000000000000000000..8eea5cee135167e9333467188c6174298ba7aa51 --- /dev/null +++ b/core/src/main/java/jenkins/security/ApiCrumbExclusion.java @@ -0,0 +1,53 @@ +/* + * 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; + +import hudson.Extension; +import hudson.security.csrf.CrumbExclusion; +import org.jenkinsci.Symbol; +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; + +/** + * JENKINS-22474: Makes API Token calls bypass CSRF protection to ease usage + */ +@Symbol("apiToken") +@Extension +@Restricted(DoNotUse.class) +public class ApiCrumbExclusion extends CrumbExclusion { + @Override + 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..1c2490052d0ee0e3bd7baa1214cecde16da96ab3 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.getUserFolder()); + } + 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/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 4a520cd82eb6c2581a9fc62dc2ddffd332c584b1..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. 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 1e71fcee5d715b0f6406b0baf52015dc1287ac87..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.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/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/SecurityListener.java b/core/src/main/java/jenkins/security/SecurityListener.java index 7cec8c0eea4afa72f446c03d781d80592c0a5443..134e992e7f662c51b89edf0cbd5d907575dc4498 100644 --- a/core/src/main/java/jenkins/security/SecurityListener.java +++ b/core/src/main/java/jenkins/security/SecurityListener.java @@ -45,39 +45,42 @@ 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. + * 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) { diff --git a/core/src/main/java/jenkins/security/SlaveToMasterCallable.java b/core/src/main/java/jenkins/security/SlaveToMasterCallable.java index 2f256f2e9d052cf7b8a14a0b39ac17bcb027f509..5da117224629b18aa3a0b2be932e34421dc2228d 100644 --- a/core/src/main/java/jenkins/security/SlaveToMasterCallable.java +++ b/core/src/main/java/jenkins/security/SlaveToMasterCallable.java @@ -6,7 +6,7 @@ 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.587 / 1.580.1 */ 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..d481361f61d26044800fe5e636958c02e4771c03 --- /dev/null +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenStats.java @@ -0,0 +1,256 @@ +/* + * 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.BulkChange; +import hudson.Util; +import hudson.XmlFile; +import hudson.model.Saveable; +import hudson.model.listeners.SaveableListener; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +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 File parent; + + public 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()); + } + + 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; + + XmlFile configFile = getConfigFile(parent); + try { + configFile.write(this); + SaveableListener.fireOnChange(this, configFile); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to save " + configFile, e); + } + } + + /** + * Loads the data from the disk into the new object. + *

+ * If the file is not present, a fresh new instance is created. + */ + public static @Nonnull ApiTokenStats load(@Nonnull 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 + + XmlFile file = getConfigFile(parent); + ApiTokenStats apiTokenStats; + if (file.exists()) { + try { + apiTokenStats = (ApiTokenStats) file.unmarshal(ApiTokenStats.class); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to load " + file, e); + apiTokenStats = new ApiTokenStats(); + } + } else { + apiTokenStats = new ApiTokenStats(); + } + + apiTokenStats.setParent(parent); + return apiTokenStats; + } + + protected static XmlFile getConfigFile(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/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/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 3a1e2b063d8a35e599ff36fb78445b1a7b717b1f..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; @@ -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/slaves/DeprecatedAgentProtocolMonitor.java b/core/src/main/java/jenkins/slaves/DeprecatedAgentProtocolMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..96886fc996278770decc16a0fd5c08985fe34b13 --- /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.getInstance().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.getInstance().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..aca5fdb54a3bc4f21593287cec346698986a4f92 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java @@ -31,11 +31,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 +76,12 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol { */ @Override public boolean isOptIn() { - return OPT_IN; + return true; + } + + @Override + public boolean isDeprecated() { + return true; } @Override @@ -99,14 +104,4 @@ public class JnlpSlaveAgentProtocol extends AgentProtocol { ExtensionList.lookup(JnlpAgentReceiver.class)); } - - /** - * 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..1c19f85a9471f3c0dd105891fb08f7a3bdde2928 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java @@ -50,7 +50,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 d996fc0aab89090b717218a5e7f8fe2e16b81b79..dac623046614292bcfd33401af879117f35f3b3c 100644 --- a/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java +++ b/core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol3.java @@ -48,15 +48,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; } /** @@ -67,6 +64,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, @@ -74,26 +76,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/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 index c82a040145ac416562bd2febce206de2b908b65c..6e998bc74d4184080f4e2e4383f1bff5ce594220 100644 --- a/core/src/main/java/jenkins/slaves/RemotingWorkDirSettings.java +++ b/core/src/main/java/jenkins/slaves/RemotingWorkDirSettings.java @@ -46,7 +46,7 @@ import org.kohsuke.stapler.DataBoundConstructor; * See Remoting Work Dir Documentation. * * @author Oleg Nenashev - * @since TODO + * @since 2.72 */ public class RemotingWorkDirSettings implements Describable { 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..3a8b9f6b4225d2fcebd863f63a7565cf8d1894cf 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()); 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/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..ea583bdb1fc870bc71d710fa1a85100f2d606f92 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/Correlator.java @@ -0,0 +1,63 @@ +/* + * 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 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; + } + + @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..65214750e237f9a3b4ef49fb59f52929396fd4a0 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/Telemetry.java @@ -0,0 +1,209 @@ +/* + * 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.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +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 + */ + @Nonnull + public abstract JSONObject createContent(); + + public static ExtensionList all() { + return ExtensionList.lookup(Telemetry.class); + } + + /** + * @since TODO + * @return whether to collect telemetry + */ + public static boolean isDisabled() { + return UsageStatistics.DISABLED || !Jenkins.get().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); + } + + JSONObject wrappedData = new JSONObject(); + wrappedData.put("type", telemetry.getId()); + wrappedData.put("payload", data); + wrappedData.put("correlator", ExtensionList.lookupSingleton(Correlator.class).getCorrelationId()); + + 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..023c7cd2019c49668f8a4b701679351669731fff --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/StaplerDispatches.java @@ -0,0 +1,120 @@ +/* + * 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 jenkins.util.SystemProperties; +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.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * 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"; + } + + @Nonnull + @Override + public JSONObject createContent() { + Map info = new TreeMap<>(); + info.put("components", buildComponentInformation()); + info.put("dispatches", buildDispatches()); + + return JSONObject.fromObject(info); + } + + private Object buildDispatches() { + Set currentTraces = traces; + traces = new TreeSet<>(); + 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 (UsageStatistics.DISABLED || !Jenkins.get().isUsageStatisticsCollected()) { + // TODO use new API after jenkinsci/jenkins#3687 is merged + // do not collect traces while usage statistics are disabled + return; + } + traces.add(s); + } + } + + private static volatile Set traces = new TreeSet<>(); +} 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..3dd93334e95c5a7312afbdc986a03ea66f5aafed --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/UserLanguages.java @@ -0,0 +1,143 @@ +/* + * 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.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Extension +@Restricted(NoExternalUse.class) +public class UserLanguages extends Telemetry { + + private static volatile Map requestsByLanguage = new TreeMap<>(); + 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); + } + + @Nonnull + @Override + public JSONObject createContent() { + JSONObject payload = new JSONObject(); + Map currentRequests = requestsByLanguage; + requestsByLanguage = new TreeMap<>(); + 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 bdf58bb7b1b410601a649c65d122faf0b7b49226..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(), Util.fixNull(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(), Util.fixNull(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, - Util.fixNull(t.upstreamProjects), p.getParent()); + 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..fcb7ec2d1acc9fa923466ae0e6753c87b91be240 100644 --- a/core/src/main/java/jenkins/util/VirtualFile.java +++ b/core/src/main/java/jenkins/util/VirtualFile.java @@ -25,29 +25,46 @@ 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.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; /** * Abstraction over {@link File}, {@link FilePath}, or other items such as network resources or ZIP entries. @@ -62,6 +79,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 +116,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 +143,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 +171,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 +262,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 +334,28 @@ 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; + } + /** * Creates a virtual file wrapper for a local file. * @param f a disk file (need not exist) @@ -250,6 +398,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 +418,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 +434,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; @@ -348,7 +509,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 +520,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 { @@ -371,14 +539,14 @@ public abstract class VirtualFile implements Comparable, Serializab } 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) { @@ -388,52 +556,65 @@ public abstract class VirtualFile implements Comparable, Serializab 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); } } } - 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/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/xstream/SafeURLConverter.java b/core/src/main/java/jenkins/util/xstream/SafeURLConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..0de0a10074a26f0e9cd62daf9b8243cca44e6d48 --- /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 TODO + */ +@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_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_it.properties b/core/src/main/resources/hudson/Messages_it.properties index 07c144a6d5d7f3750824d8d5b60a23336641b43c..48deedea6948aa3e19a70a659b0af63539843848 100644 --- a/core/src/main/resources/hudson/Messages_it.properties +++ b/core/src/main/resources/hudson/Messages_it.properties @@ -1,56 +1,62 @@ -# 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. - -FilePath.validateAntFileMask.whitespaceSeprator=\ - Lo spazio non pu\u00f2 pi\u00f9 essere usato come separatore. Usare la virgola '','' per separare. +FilePath.did_not_manage_to_validate_may_be_too_sl=Impossibile validare {0} (potrebbe essere troppo lento) +FilePath.validateAntFileMask.whitespaceSeparator=\ + Non pi possibile utilizzare spazi bianchi come separatore. Si utilizzi invece "," come separatore. FilePath.validateAntFileMask.doesntMatchAndSuggest=\ - ''{0}'' non corrisponde, ma ''{1}'' \u00e8 valido. Forse intendevi questo ? -FilePath.validateAntFileMask.portionMatchAndSuggest=''{0}'' non corrisponde a nulla, anche se ''{1}'' esiste -FilePath.validateAntFileMask.portionMatchButPreviousNotMatchAndSuggest=''{0}'' non corrisponde: ''{1}'' esiste ma non ''{2}'' -FilePath.validateAntFileMask.doesntMatchAnything=''{0}'' non corrisponde -FilePath.validateAntFileMask.doesntMatchAnythingAndSuggest=''{0}'' non corrisponde: anche ''{1}'' non esiste - -FilePath.validateRelativePath.wildcardNotAllowed=Non sono ammessi caratteri jolly -FilePath.validateRelativePath.notFile=''{0}'' non \u00e8 un file -FilePath.validateRelativePath.notDirectory=''{0}'' non \u00e8 una cartella -FilePath.validateRelativePath.noSuchFile=File non trovato: ''{0}'' -FilePath.validateRelativePath.noSuchDirectory=Cartella non trovata: ''{0}'' + Non vi sono corrispondenze per "{0}", ma per "{1}" s. Forse era ci che si intendeva? +FilePath.validateAntFileMask.portionMatchAndSuggest=Non vi sono corrispondenze per "{0}", bench "{1}" esista +FilePath.validateAntFileMask.portionMatchButPreviousNotMatchAndSuggest=Non vi sono corrispondenze per "{0}": "{1}" esiste ma "{2}" no +FilePath.validateAntFileMask.doesntMatchAnything=Non vi sono corrispondenze per "{0}" +FilePath.validateAntFileMask.matchWithCaseInsensitive=Non vi sono corrispondenze per "{0}" perch si fa differenza tra maiuscole e minuscole. possibile disattivare Maiuscole/minuscole per ottenere delle corrispondenze +FilePath.validateAntFileMask.doesntMatchAnythingAndSuggest=Non vi sono corrispondenze per "{0}": non esiste nemmeno "{1}" +FilePath.validateRelativePath.wildcardNotAllowed=Non sono consentiti caratteri jolly qui +FilePath.validateRelativePath.notFile="{0}" non un file +FilePath.validateRelativePath.notDirectory="{0}" non una directory +FilePath.validateRelativePath.noSuchFile=File non esistente: "{0}" +FilePath.validateRelativePath.noSuchDirectory=Directory non esistente: "{0}" + +PluginManager.PluginDoesntSupportDynamicLoad.RestartRequired=Il plugin {0} non supporta il caricamento dinamico. Jenkins deve essere riavviato per completare l''aggiornamento +PluginManager.PluginIsAlreadyInstalled.RestartRequired=Il plugin {0} gi installato. Jenkins deve essere riavviato per completare l''aggiornamento +Util.second={0} s +Util.hour ={0} h +Util.day ={0} g +Util.month ={0} m +Util.year ={0} a +Util.pastTime={0} Util.millisecond={0} ms -Util.second={0} sec Util.minute={0} min -Util.hour ={0} hr -Util.day ={0} {0,choice,0#days|1#day|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/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/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_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..26dafbc2fce892ee123e04efaa7614d9ba39e7a0 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. + @@ -105,10 +106,10 @@ THE SOFTWARE.
${%coreWarning(p.requiredCore)}
- +
${%depCompatWarning}
- +
${%depCoreWarning(p.getNeededDependenciesRequiredCore().toString())}
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_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..df4e3f512568edbe01026905383f6648af16063f 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=\ 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_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. -