diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 456f8fca6da7..a2ed88450df5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -28,16 +28,17 @@ For refactoring and code cleanup changes, exercise the code before and after the ### Proposed changelog entries -- JENKINS-XXXXX, human-readable text +- human-readable text @@ -45,6 +46,11 @@ You may add multiple changelog entries if applicable by adding a new entry to th N/A + + ```[tasklist] ### Submitter checklist - [ ] The Jira issue, if it exists, is well-described. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index a44d07b50c54..000000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "maven" - directory: "/" - schedule: - interval: "daily" - ignore: - # Exclusions in this section have been triaged and determined to be - # permanent. We do not anticipate removing exclusions from this section. - - # Provided by Jetty and should be aligned with the version provided by the - # version of Jetty we deliver. See: - # https://github.com/jenkinsci/jenkins/pull/5211 - - dependency-name: "jakarta.servlet:jakarta.servlet-api" - - # Jetty Maven Plugin and Winstone should be upgraded in lockstep in order - # to keep their corresponding Jetty versions aligned. - - dependency-name: "org.eclipse.jetty:jetty-maven-plugin" - - dependency-name: "org.jenkins-ci:winstone" - - # Here lies technical debt. Exclusions in this section have been triaged - # and determined to be temporary. Exclusions should be removed from this - # section once the remaining action items have been completed. - - # Contains incompatible API changes and needs compatibility work. - - dependency-name: "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api" - - # Needs significant testing. See: - # https://github.com/jenkinsci/jenkins/pull/5112#issuecomment-744429487 - # https://github.com/jenkinsci/jenkins/pull/5116#issuecomment-744526638 - - dependency-name: "org.codehaus.groovy:groovy-all" - versions: [">=2.5.0"] - - # Consumed by Groovy and should be updated in lockstep with Groovy. See: - # https://github.com/jenkinsci/jenkins/pull/5184 - - dependency-name: "org.fusesource.jansi:jansi" - - # Contains incompatible API changes and needs compatibility work. See: - # https://github.com/jenkinsci/jenkins/pull/4224 - - dependency-name: "org.jfree:jfreechart" - - # Starting with 6.x, Spring requires Java 17 at a minimum. - - dependency-name: "org.springframework:spring-framework-bom" - versions: [">=6.0.0"] - - # Starting with 6.x, Spring Security requires Java 17 at a minimum. - - dependency-name: "org.springframework.security:spring-security-bom" - versions: [">=6.0.0"] - - # Starting with 7.x, Guice switches from javax.* to jakarta.* bindings. - # See https://github.com/google/guice/wiki/Guice700 - - dependency-name: "com.google.inject:guice-bom" - versions: [">=7.0.0"] - - package-ecosystem: "maven" - directory: "/" - target-branch: "stable-2.440" - labels: - - "into-lts" - - "needs-justification" - schedule: - interval: "daily" - # Include only security updates and exclude version updates. - open-pull-requests-limit: 0 diff --git a/.github/renovate.json b/.github/renovate.json index 8c3c4ad17cea..aab994ad8187 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,63 +1,221 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base", + "config:recommended", ":disableDependencyDashboard", ":semanticCommitsDisabled" ], - "enabledManagers": ["npm", "regex"], - "postUpdateOptions": ["yarnDedupeHighest"], + "prHourlyLimit": 0, + "prConcurrentLimit": 0, + "postUpdateOptions": [ + "yarnDedupeHighest" + ], "packageRules": [ { - "matchDatasources": ["npm"], - "addLabels": ["javascript"], - "stabilityDays": 3, - "reviewers": ["team:sig-ux"] + "matchDatasources": [ + "npm" + ], + "addLabels": [ + "javascript" + ], + "minimumReleaseAge": "3 days", + "reviewers": [ + "team:sig-ux" + ] }, { - "matchPackageNames": ["node"], + "matchPackageNames": [ + "node" + ], "allowedVersions": "/20.[0-9]+.[0-9]+(.[0-9]+)?$/" + }, + { + "description": "Should be upgraded in lockstep in order to keep their corresponding Jetty versions aligned, could be grouped but releases are likely separated by a bit of time", + "matchManagers": [ + "maven" + ], + "enabled": false, + "matchPackageNames": [ + "org.eclipse.jetty:jetty-maven-plugin", + "org.jenkins-ci:winstone" + ] + }, + { + "description": "Provided by Jetty and should be aligned with the version provided by the version of Jetty we deliver. See: https://github.com/jenkinsci/jenkins/pull/5211", + "matchManagers": [ + "maven" + ], + "enabled": false, + "matchPackageNames": [ + "jakarta.servlet:jakarta.servlet-api", + "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api" + ] + }, + { + "description": "Needs significant testing. See: https://github.com/jenkinsci/jenkins/pull/5112#issuecomment-744429487 and https://github.com/jenkinsci/jenkins/pull/5116#issuecomment-744526638", + "matchManagers": [ + "maven" + ], + "allowedVersions": "<2.5.0", + "matchPackageNames": [ + "org.codehaus.groovy:groovy-all" + ] + }, + { + "description": "Consumed by Groovy and should be updated in lockstep with Groovy. See: https://github.com/jenkinsci/jenkins/pull/5184", + "matchManagers": [ + "maven" + ], + "enabled": false, + "matchPackageNames": [ + "org.fusesource.jansi:jansi" + ] + }, + { + "description": "Depends on commons-lang3 which is in progress for removal from core. See: https://issues.jenkins.io/browse/JENKINS-73355", + "matchManagers": [ + "maven" + ], + "enabled": false, + "matchPackageNames": [ + "org.apache.commons:commons-compress" + ] + }, + { + "description": "Contains incompatible API changes and needs compatibility work. See: https://github.com/jenkinsci/jenkins/pull/4224", + "matchManagers": [ + "maven" + ], + "enabled": false, + "matchPackageNames": [ + "org.jfree:jfreechart" + ] + }, + { + "description": "Starting with 6.x, Spring requires Java 17 at a minimum.", + "matchManagers": [ + "maven" + ], + "allowedVersions": "<6.0.0", + "matchPackageNames": [ + "org.springframework:spring-framework-bom", + "org.springframework.security:spring-security-bom" + ] + }, + { + "description": "Starting with 7.x, Guice switches from javax.* to jakarta.* bindings. See https://github.com/google/guice/wiki/Guice700", + "matchManagers": [ + "maven" + ], + "allowedVersions": "<7.0.0", + "matchPackageNames": [ + "com.google.inject:guice-bom" + ] + }, + { + "matchFileNames": [ + "core/pom.xml", + "test/pom.xml", + "war/pom.xml" + ], + "matchPackageNames": [ + "org.jenkins-ci.main:remoting" + ], + "description": "Avoid updating the remoting.minimum.supported.version property but still update latest one by not placing this property in the parent pom.xml", + "enabled": false + }, + { + "matchPackageNames": [ + "net.jcip:jcip-annotations" + ], + "matchDatasources": [ + "maven" + ], + "enabled": false, + "description": "maven-metadata.xml is missing for this really old package which is required by renovate" } ], - "regexManagers": [ + "customManagers": [ { - "fileMatch": ["war/pom.xml"], - "matchStrings": ["(?.*?)"], + "customType": "regex", + "fileMatch": [ + "war/pom.xml" + ], + "matchStrings": [ + "(?.*?)" + ], "depNameTemplate": "node", "datasourceTemplate": "npm" }, { - "fileMatch": ["ath.sh"], - "matchStrings": ["export ATH_VERSION=(?.*?)\n"], + "customType": "regex", + "fileMatch": [ + "ath.sh" + ], + "matchStrings": [ + "export ATH_VERSION=(?.*?)\n" + ], "depNameTemplate": "jenkins/ath", "datasourceTemplate": "docker", "versioningTemplate": "loose" }, { - "fileMatch": [".gitpod/Dockerfile"], - "matchStrings": ["ARG MAVEN_VERSION=(?.*?)\n"], + "customType": "regex", + "fileMatch": [ + ".gitpod/Dockerfile" + ], + "matchStrings": [ + "ARG MAVEN_VERSION=(?.*?)\n" + ], "depNameTemplate": "org.apache.maven:maven-core", "datasourceTemplate": "maven" }, { - "fileMatch": ["core/src/site/site.xml"], - "matchStrings": ["lit@(?.*?)/"], + "customType": "regex", + "fileMatch": [ + "core/src/site/site.xml" + ], + "matchStrings": [ + "lit@(?.*?)/" + ], "depNameTemplate": "lit", "datasourceTemplate": "npm" }, { - "fileMatch": ["core/src/site/site.xml"], - "matchStrings": ["webcomponentsjs@(?.*?)/"], + "customType": "regex", + "fileMatch": [ + "core/src/site/site.xml" + ], + "matchStrings": [ + "webcomponentsjs@(?.*?)/" + ], "depNameTemplate": "@webcomponents/webcomponentsjs", "datasourceTemplate": "npm" }, { - "fileMatch": ["core/src/site/site.xml"], - "matchStrings": ["(?.*?)<\/version>"], + "customType": "regex", + "fileMatch": [ + "core/src/site/site.xml" + ], + "matchStrings": [ + "(?.*?)" + ], "depNameTemplate": "org.apache.maven.skins:maven-fluido-skin", "datasourceTemplate": "maven" } ], - "labels": ["dependencies", "skip-changelog"], - "rebaseWhen": "conflicted" + "labels": [ + "dependencies", + "skip-changelog" + ], + "rebaseWhen": "conflicted", + "ignorePaths": [ + "**/node_modules/**", + "**/bower_components/**", + "**/vendor/**", + "**/examples/**", + "**/__tests__/**", + "**/tests/**", + "**/__fixtures__/**" + ] } diff --git a/.github/workflows/label-conflicting-pr.yml b/.github/workflows/label-conflicting-pr.yml index 80079296c113..8b78edd6004c 100644 --- a/.github/workflows/label-conflicting-pr.yml +++ b/.github/workflows/label-conflicting-pr.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Label conflicting PRs - uses: eps1lon/actions-label-merge-conflict@v3.0.0 + uses: eps1lon/actions-label-merge-conflict@v3.0.2 with: dirtyLabel: "unresolved-merge-conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/publish-release-artifact.yml b/.github/workflows/publish-release-artifact.yml index 968df18529c9..8b7242136ce9 100644 --- a/.github/workflows/publish-release-artifact.yml +++ b/.github/workflows/publish-release-artifact.yml @@ -73,7 +73,7 @@ jobs: wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME} - name: Upload Release Asset id: upload-war - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -108,7 +108,7 @@ jobs: - name: Upload Release Asset id: upload-deb if: always() - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -144,7 +144,7 @@ jobs: - name: Upload Release Asset id: upload-rpm if: always() - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -180,7 +180,7 @@ jobs: - name: Upload Release Asset id: upload-msi if: always() - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -216,7 +216,7 @@ jobs: - name: Upload Release Asset id: upload-suse-rpm if: always() - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile index 6b71c6707163..58d3fa8a87f4 100644 --- a/.gitpod/Dockerfile +++ b/.gitpod/Dockerfile @@ -1,6 +1,6 @@ FROM gitpod/workspace-full -ARG MAVEN_VERSION=3.9.6 +ARG MAVEN_VERSION=3.9.9 RUN brew install gh && \ bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && sdk install maven ${MAVEN_VERSION} && sdk default maven ${MAVEN_VERSION}" diff --git a/.idea/encodings.xml b/.idea/encodings.xml index de5572116383..ca018ebc3ab9 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -32,6 +32,9 @@ + + + diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index a64ab6f242ca..30a03ea18d31 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,6 +2,6 @@ io.jenkins.tools.incrementals git-changelist-maven-extension - 1.7 + 1.8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77c012b44c68..5933560a951b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,13 +9,11 @@ This page provides information about contributing code to the Jenkins core codeb 1. Fork the repository on GitHub 2. Clone the forked repository to your machine 3. Install the necessary development tools. In order to develop Jenkins, you need the following: - - Java Development Kit (JDK) 11, 17 or 21. + - Java Development Kit (JDK) 17 or 21. In the Jenkins project we usually use [Eclipse Temurin](https://adoptium.net/) or [OpenJDK](https://openjdk.java.net/), but you can use other JDKs as well. - Apache Maven 3.8.1 or above. You can [download Maven here](https://maven.apache.org/download.cgi). In the Jenkins project we usually use the most recent Maven release. - Any IDE which supports importing Maven projects. - - Install [Node.js 20.x](https://nodejs.org/en/). **Note:** only needed to work on the frontend assets found in the `war` module. - - Frontend tasks are run using [yarn](https://yarnpkg.com/). Run `npm install -g yarn` to install it. 4. Set up your development environment as described in [Preparing for Plugin Development](https://www.jenkins.io/doc/developer/tutorial/prepare/) If you want to contribute to Jenkins, or just learn about the project, @@ -28,6 +26,8 @@ You can find them by using this query (check the link) for [newbie friendly issu The Jenkins core build flow is built around Maven. You can read a description of the [building and debugging process here](https://www.jenkins.io/doc/developer/building/). +### Building the WAR file + If you want simply to build the `jenkins.war` file as fast as possible without tests, run: ```sh @@ -40,14 +40,31 @@ If you want to debug the WAR file without using Maven plugins, You can run the executable with [Remote Debug Flags](https://stackoverflow.com/questions/975271/remote-debugging-a-java-application) and then attach IDE Debugger to it. -To launch a development instance, after the above command, run: +### Launching a development instance + +To launch a development instance, after [building the WAR file](#building-the-war-file), run: ```sh -mvn -pl war jetty:run +MAVEN_OPTS='--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED' mvn -pl war jetty:run ``` (Beware that `maven-plugin` builds will not work in this mode, due to class loading conflicts.) +### Running the Yarn frontend build + +To run the Yarn frontend build, after [building the WAR file](#building-the-war-file), add the downloaded versions of Node and Yarn to your path: + +```sh +export PATH=$PWD/war/node:$PWD/war/node/yarn/dist/bin:$PATH +``` + +Then you can run Yarn with e.g. + +```sh +cd war +yarn +``` + ### Building frontend assets To work on the `war` module frontend assets, two processes are needed at the same time: @@ -55,13 +72,14 @@ To work on the `war` module frontend assets, two processes are needed at the sam On one terminal, start a development server that will not process frontend assets: ```sh -mvn -pl war jetty:run -Dskip.yarn +MAVEN_OPTS='--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED' mvn -pl war jetty:run -Dskip.yarn ``` -On another terminal, move to the war folder and start a [webpack](https://webpack.js.org/) dev server: +On another terminal, move to the `war` folder and start a [webpack](https://webpack.js.org/) dev server, after [adding Node and Yarn to your path](#running-the-yarn-frontend-build): ```sh -cd war; yarn start +cd war +yarn start ``` ### Gitpod @@ -86,11 +104,24 @@ For linting we use a number of tools: These are all configured to run as part of the Maven build, although they will be skipped if you are building with the `quick-build` profile. -To automatically fix most issues run: +To automatically fix backend issues, run: -```bash +```sh mvn spotless:apply -mvn -pl war frontend:yarn -Dfrontend.yarn.arguments=lint:fix +``` + +To view frontend issues, after [adding Node and Yarn to your path](#running-the-yarn-frontend-build), run: + +```sh +cd war +yarn lint +``` + +To fix frontend issues, after [adding Node and Yarn to your path](#running-the-yarn-frontend-build), run: + +```sh +cd war +yarn lint:fix ``` ## Testing changes @@ -111,14 +142,6 @@ 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. -### JavaScript unit tests - -In case there's only need to run the JS tests: - -```sh -cd war; yarn test -``` - ## Proposing Changes The Jenkins project source code repositories are hosted at GitHub. diff --git a/Jenkinsfile b/Jenkinsfile index 8b87983f43fc..2380b3b66d9a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,12 +14,12 @@ properties([ def axes = [ platforms: ['linux', 'windows'], - jdks: [11, 17, 21], + jdks: [17, 21], ] stage('Record build') { retry(conditions: [kubernetesAgent(handleNonKubernetes: true), nonresumable()], count: 2) { - node('maven-11') { + node('maven-17') { infra.checkoutSCM() /* diff --git a/README.md b/README.md index e41867cad4b9..e69e30927214 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![Jenkins LTS Release](https://img.shields.io/endpoint?url=https%3A%2F%2Fwww.jenkins.io%2Fchangelog-stable%2Fbadge.json)](https://www.jenkins.io/changelog-stable) [![Docker Pulls](https://img.shields.io/docker/pulls/jenkins/jenkins.svg)](https://hub.docker.com/r/jenkins/jenkins/) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3538/badge)](https://bestpractices.coreinfrastructure.org/projects/3538) +[![Reproducible Builds](https://img.shields.io/badge/Reproducible_Builds-ok-green)](https://maven.apache.org/guides/mini/guide-reproducible-builds.html) [![Gitter](https://img.shields.io/gitter/room/jenkinsci/jenkins)](https://app.gitter.im/#/room/#jenkinsci_jenkins:gitter.im) In a nutshell, Jenkins is the leading open-source automation server. diff --git a/ath.sh b/ath.sh index e39c053ef66e..415a9c6bd72d 100644 --- a/ath.sh +++ b/ath.sh @@ -6,7 +6,7 @@ set -o xtrace cd "$(dirname "$0")" # https://github.com/jenkinsci/acceptance-test-harness/releases -export ATH_VERSION=5814.vdc5d6d484b_40 +export ATH_VERSION=5941.v95f3439136c7 if [[ $# -eq 0 ]]; then export JDK=17 diff --git a/bom/pom.xml b/bom/pom.xml index 61dee6848809..ad23ed312b5e 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -38,9 +38,8 @@ THE SOFTWARE. The module contains dependencies that are used by a specific Jenkins version - 9.7 - 2.0.12 - 1843.ve7da_6a_9cf575 + 2.0.0-M2 + 1896.v8170998149d0 2.4.21 @@ -53,10 +52,17 @@ THE SOFTWARE. pom import + + org.slf4j + slf4j-bom + 2.0.16 + pom + import + org.springframework spring-framework-bom - 5.3.33 + 5.3.39 pom import @@ -64,7 +70,7 @@ THE SOFTWARE. org.springframework.security spring-security-bom - 5.8.11 + 5.8.14 pom import @@ -72,7 +78,7 @@ THE SOFTWARE. args4j args4j - 2.33 + 2.37 com.github.spotbugs @@ -82,7 +88,7 @@ THE SOFTWARE. com.google.guava guava - 33.1.0-jre + 33.3.0-jre @@ -113,18 +119,13 @@ THE SOFTWARE. commons-codec commons-codec - 1.16.1 + 1.17.1 commons-collections commons-collections 3.2.2 - - commons-fileupload - commons-fileupload - 1.5 - commons-io commons-io @@ -150,6 +151,11 @@ THE SOFTWARE. jenkins-stapler-support 1.1 + + jakarta.servlet + jakarta.servlet-api + 4.0.4 + jakarta.servlet.jsp.jstl jakarta.servlet.jsp.jstl-api @@ -190,6 +196,41 @@ THE SOFTWARE. commons-compress 1.26.1 + + org.apache.commons + commons-fileupload2 + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-core + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-distribution + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-jakarta-servlet5 + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-jakarta-servlet6 + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-javax + ${commons-fileupload2.version} + + + org.apache.commons + commons-fileupload2-portlet + ${commons-fileupload2.version} + org.codehaus.groovy groovy-all @@ -254,7 +295,7 @@ THE SOFTWARE. org.jvnet.hudson commons-jelly-tags-define - 1.1-jenkins-20230713 + 1.1-jenkins-20240510 org.jvnet.localizer @@ -269,7 +310,7 @@ THE SOFTWARE. org.jvnet.winp winp - 1.30 + 1.31 org.kohsuke @@ -294,7 +335,7 @@ THE SOFTWARE. org.kohsuke.stapler json-lib - 2.4-jenkins-3 + 2.4-jenkins-7 org.kohsuke.stapler @@ -311,62 +352,16 @@ THE SOFTWARE. stapler-groovy ${stapler.version} - - org.ow2.asm - asm - ${asm.version} - - - org.ow2.asm - asm-analysis - ${asm.version} - - - org.ow2.asm - asm-commons - ${asm.version} - - - org.ow2.asm - asm-tree - ${asm.version} - - - org.ow2.asm - asm-util - ${asm.version} - org.samba.jcifs jcifs 1.3.18-kohsuke-1 - - org.slf4j - jcl-over-slf4j - ${slf4jVersion} - - - org.slf4j - log4j-over-slf4j - ${slf4jVersion} - - - - org.slf4j - slf4j-api - ${slf4jVersion} - - - org.slf4j - slf4j-jdk14 - ${slf4jVersion} - commons-logging commons-logging - 1.3.1 + 1.3.4 provided diff --git a/cli/pom.xml b/cli/pom.xml index 342b4061016d..6309811a2e8b 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -15,7 +15,7 @@ https://github.com/jenkinsci/jenkins - 2.12.1 + 2.13.2 @@ -65,7 +65,7 @@ org.glassfish.tyrus.bundles tyrus-standalone-client-jdk - 2.1.5 + 2.2.0 true @@ -119,7 +119,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.2 + 3.6.0 diff --git a/core/pom.xml b/core/pom.xml index 3d1f16532c01..8e30ae211ed7 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -39,7 +39,9 @@ THE SOFTWARE. https://github.com/jenkinsci/jenkins - 2.9.1 + 2.10.0 + + 3107.v665000b_51092 @@ -160,10 +162,6 @@ THE SOFTWARE. commons-collections commons-collections - - commons-fileupload - commons-fileupload - commons-io commons-io @@ -284,6 +282,14 @@ THE SOFTWARE. + + org.apache.commons + commons-fileupload2-core + + + org.apache.commons + commons-fileupload2-javax + org.codehaus.groovy groovy-all @@ -409,26 +415,6 @@ THE SOFTWARE. - - org.ow2.asm - asm - - - org.ow2.asm - asm-analysis - - - org.ow2.asm - asm-commons - - - org.ow2.asm - asm-tree - - - org.ow2.asm - asm-util - org.slf4j jcl-over-slf4j @@ -459,7 +445,6 @@ THE SOFTWARE. jakarta.servlet jakarta.servlet-api - 4.0.4 provided @@ -524,6 +509,62 @@ THE SOFTWARE. + + org.apache.maven.plugins + maven-enforcer-plugin + + + + enforce-banned-dependencies + + enforce + + + + + + + com.fasterxml.jackson.* + com.github.ben-manes.caffeine:caffeine + com.github.jnr:jnr-posix + com.github.mwiede:jsch + com.google.code.gson:gson + com.jayway.jsonpath:json-path + commons-httpclient:commons-httpclient + com.sun.activation:javax.activation + com.sun.mail:javax.mail + com.sun.xml.bind:jaxb-impl + io.jsonwebtoken + + jakarta.activation:jakarta.activation-api:*:jar:compile + jakarta.activation:jakarta.activation-api:*:jar:runtime + jakarta.mail:jakarta.mail-api + javax.activation:javax.activation-api + javax.mail:javax.mail-api + javax.xml.bind:jaxb-api + joda-time:joda-time + + net.bytebuddy:byte-buddy:*:jar:compile + net.bytebuddy:byte-buddy:*:jar:runtime + net.i2p.crypto:eddsa + net.minidev + org.apache.commons:commons-lang3 + org.apache.commons:commons-text + org.apache.httpcomponents + org.bouncycastle + org.eclipse.angus:angus-activation + org.eclipse.angus:angus-mail + org.glassfish.jersey.* + org.json:json + org.ow2.asm + org.yaml:snakeyaml + + + + + + + org.codehaus.mojo build-helper-maven-plugin diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index 10b0d179fdd7..3d6edf832f9f 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -52,6 +52,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -235,7 +236,11 @@ private static Manifest loadLinkedManifest(File archive) throws IOException { dependencyLoader = getBaseClassLoader(atts, dependencyLoader); return new PluginWrapper(pluginManager, archive, manifest, baseResourceURL, - createClassLoader(paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies); + createClassLoader(computeClassLoaderName(manifest, archive), paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies); + } + + private static String computeClassLoaderName(Manifest mf, File archive) { + return "PluginClassLoader for " + PluginWrapper.computeShortName(mf, archive.getName()); } private void fix(Attributes atts, List optionalDependencies) { @@ -247,7 +252,7 @@ private void fix(Attributes atts, List optionalDepende for (Dependency d : DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion)) { LOGGER.fine(() -> "implied dep " + pluginName + " → " + d.shortName); - pluginManager.considerDetachedPlugin(d.shortName); + pluginManager.considerDetachedPlugin(d.shortName, pluginName); optionalDependencies.add(d); } } @@ -263,15 +268,28 @@ public static List getImpliedDependencies(String plugi return DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion); } - @Deprecated + /** + * @deprecated since 2.459 use {@link #createClassLoader(String, List, ClassLoader, Attributes)} + */ + @Deprecated(since = "2.459") protected ClassLoader createClassLoader(List paths, ClassLoader parent) throws IOException { return createClassLoader(paths, parent, null); } /** - * Creates the classloader that can load all the specified jar files and delegate to the given parent. + * @deprecated since 2.459 use {@link #createClassLoader(String, List, ClassLoader, Attributes)} */ + @Deprecated(since="2.459") protected ClassLoader createClassLoader(List paths, ClassLoader parent, Attributes atts) throws IOException { + // generate a legacy id so at least we can track to something + return createClassLoader("unidentified-" + UUID.randomUUID(), paths, parent, atts); + } + + /** + * Creates a classloader that can load all the specified jar files and delegate to the given parent. + * @since 2.459 + */ + protected ClassLoader createClassLoader(String name, List paths, ClassLoader parent, Attributes atts) throws IOException { boolean usePluginFirstClassLoader = atts != null && Boolean.parseBoolean(atts.getValue("PluginFirstClassLoader")); @@ -285,9 +303,9 @@ protected ClassLoader createClassLoader(List paths, ClassLoader parent, At } URLClassLoader2 classLoader; if (usePluginFirstClassLoader) { - classLoader = new PluginFirstClassLoader2(urls.toArray(new URL[0]), parent); + classLoader = new PluginFirstClassLoader2(name, urls.toArray(new URL[0]), parent); } else { - classLoader = new URLClassLoader2(urls.toArray(new URL[0]), parent); + classLoader = new URLClassLoader2(name, urls.toArray(new URL[0]), parent); } return classLoader; } @@ -561,7 +579,7 @@ static final class DependencyClassLoader extends ClassLoader { } DependencyClassLoader(ClassLoader parent, File archive, List dependencies, PluginManager pluginManager) { - super(parent); + super("dependency ClassLoader for " + archive.getPath(), parent); this._for = archive; this.dependencies = List.copyOf(dependencies); this.pluginManager = pluginManager; diff --git a/core/src/main/java/hudson/DependencyRunner.java b/core/src/main/java/hudson/DependencyRunner.java index b7db91fb9416..f577440659cf 100644 --- a/core/src/main/java/hudson/DependencyRunner.java +++ b/core/src/main/java/hudson/DependencyRunner.java @@ -58,7 +58,7 @@ public void run() { // Get all top-level projects LOGGER.fine("assembling top level projects"); for (AbstractProject p : Jenkins.get().allItems(AbstractProject.class)) - if (p.getUpstreamProjects().size() == 0) { + if (p.getUpstreamProjects().isEmpty()) { LOGGER.fine("adding top level project " + p.getName()); topLevelProjects.add(p); } else { diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java index 286151e46bf7..97def5f11a7f 100644 --- a/core/src/main/java/hudson/EnvVars.java +++ b/core/src/main/java/hudson/EnvVars.java @@ -114,8 +114,7 @@ public EnvVars(@NonNull Map m) { // because of the backward compatibility, some parts of Jenkins passes // EnvVars as Map so downcasting is safer. - if (m instanceof EnvVars) { - EnvVars lhs = (EnvVars) m; + if (m instanceof EnvVars lhs) { this.platform = lhs.platform; } } diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index 801735835c0f..7301bcc39988 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -298,9 +298,11 @@ protected Injector resolve() { } private void refreshExtensionAnnotations() { + LOGGER.finer(() -> "refreshExtensionAnnotations()"); for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) { GuiceExtensionAnnotation gea = ec.getInstance(); extensionAnnotations.put(gea.annotationType, gea); + LOGGER.finer(() -> "found " + gea.getClass()); } } @@ -328,6 +330,7 @@ public Injector getContainer() { */ @Override public synchronized ExtensionComponentSet refresh() throws ExtensionRefreshException { + LOGGER.finer(() -> "refresh()"); refreshExtensionAnnotations(); // figure out newly discovered sezpoz components List> delta = new ArrayList<>(); diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 3525b931ef78..06773dd9a9ee 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -31,8 +31,6 @@ import static hudson.Util.fixEmptyAndTrim; import com.google.common.annotations.VisibleForTesting; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -61,7 +59,6 @@ import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.FileVisitor; import hudson.util.FormValidation; -import hudson.util.HeadBufferingStream; import hudson.util.IOUtils; import hudson.util.NamingThreadFactory; import hudson.util.io.Archiver; @@ -80,6 +77,7 @@ import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.Serializable; +import java.io.UncheckedIOException; import java.io.Writer; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -123,6 +121,8 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import jenkins.MasterToSlaveFileCallable; import jenkins.SlaveToMasterFileCallable; import jenkins.model.Jenkins; @@ -130,14 +130,14 @@ 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.fileupload2.core.FileItem; import org.apache.commons.io.input.CountingInputStream; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileSet; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarInputStream; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipFile; import org.jenkinsci.remoting.RoleChecker; @@ -321,7 +321,7 @@ public static String normalize(@NonNull String path) { buf.append(m.group(1)); path = path.substring(m.end()); } - boolean isAbsolute = buf.length() > 0; + boolean isAbsolute = !buf.isEmpty(); // Split remaining path into tokens, trimming any duplicate or trailing separators List tokens = new ArrayList<>(); int s = 0, end = path.length(); @@ -366,7 +366,7 @@ public static String normalize(@NonNull String path) { } // Recombine tokens for (String token : tokens) buf.append(token); - if (buf.length() == 0) buf.append('.'); + if (buf.isEmpty()) buf.append('.'); return buf.toString(); } @@ -887,15 +887,8 @@ public OutputStream compress(OutputStream out) { }, GZIP { @Override - public InputStream extract(InputStream _in) throws IOException { - HeadBufferingStream in = new HeadBufferingStream(_in, SIDE_BUFFER_SIZE); - try { - return new GZIPInputStream(in, 8192, true); - } catch (IOException e) { - // various people reported "java.io.IOException: Not in GZIP format" here, so diagnose this problem better - in.fillSide(); - throw new IOException(e.getMessage() + "\nstream=" + Util.toHexString(in.getSideBuffer()), e); - } + public InputStream extract(InputStream in) throws IOException { + return new GZIPInputStream(new BufferedInputStream(in)); } @Override @@ -1004,8 +997,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } } - if (con instanceof HttpURLConnection) { - HttpURLConnection httpCon = (HttpURLConnection) con; + if (con instanceof HttpURLConnection httpCon) { int responseCode = httpCon.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { @@ -1166,7 +1158,9 @@ public void copyFrom(FilePath src) throws IOException, InterruptedException { public void copyFrom(FileItem file) throws IOException, InterruptedException { if (channel == null) { try { - file.write(new File(remote)); + file.write(Paths.get(remote)); + } catch (UncheckedIOException e) { + throw e.getCause(); } catch (IOException e) { throw e; } catch (Exception e) { @@ -1180,6 +1174,14 @@ public void copyFrom(FileItem file) throws IOException, InterruptedException { } } + /** + * @deprecated use {@link #copyFrom(FileItem)} + */ + @Deprecated + public void copyFrom(org.apache.commons.fileupload.FileItem file) throws IOException, InterruptedException { + copyFrom(file.toFileUpload2FileItem()); + } + /** * Code that gets executed on the machine where the {@link FilePath} is local. * Used to act on {@link FilePath}. @@ -1442,7 +1444,7 @@ private static class DeleteSuffixesRecursive extends MasterToSlaveFileCallable path.toFile()); + Util.deleteRecursive(file.toPath(), Path::toFile); } } @@ -1473,7 +1475,7 @@ private static class DeleteRecursive extends MasterToSlaveFileCallable { @Override public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteRecursive(fileToPath(f), path -> path.toFile()); + Util.deleteRecursive(fileToPath(f), Path::toFile); return null; } } @@ -1490,7 +1492,7 @@ private static class DeleteContents extends MasterToSlaveFileCallable { @Override public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteContentsRecursive(fileToPath(f), path -> path.toFile()); + Util.deleteContentsRecursive(fileToPath(f), Path::toFile); return null; } } @@ -2460,7 +2462,7 @@ private OffsetPipeSecureFileCallable(Pipe p, long offset) { @Override public Void invoke(File f, VirtualChannel channel) throws IOException { try (OutputStream os = p.getOut(); - OutputStream out = new java.util.zip.GZIPOutputStream(os, 8192); + OutputStream out = new GZIPOutputStream(os, 8192); RandomAccessFile raf = new RandomAccessFile(f, "r")) { raf.seek(offset); byte[] buf = new byte[8192]; @@ -3068,14 +3070,13 @@ private static void readFromTar(String name, File baseDir, InputStream in) throw /** * Reads from a tar stream and stores obtained files to the base dir. - * Supports large files > 10 GB since 1.627 when this was migrated to use commons-compress. + * Supports large files > 10 GB since 1.627. */ private static void readFromTar(String name, File baseDir, InputStream in, Charset filenamesEncoding) throws IOException { - // TarInputStream t = new TarInputStream(in); - try (TarArchiveInputStream t = new TarArchiveInputStream(in, filenamesEncoding.name())) { - TarArchiveEntry te; - while ((te = t.getNextTarEntry()) != null) { + try (TarInputStream t = new TarInputStream(in, filenamesEncoding.name())) { + TarEntry te; + while ((te = t.getNextEntry()) != null) { File f = new File(baseDir, te.getName()); if (!f.toPath().normalize().startsWith(baseDir.toPath())) { throw new IOException( diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 2e6abc978540..cd25f9bc9871 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -316,8 +316,7 @@ public static void initPageVariables(JellyContext context) { */ public static Class getTypeParameter(Class c, Class base, int n) { Type parameterization = Types.getBaseClass(c, base); - if (parameterization instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) parameterization; + if (parameterization instanceof ParameterizedType pt) { return Types.erasure(Types.getTypeArgument(pt, n)); } else { throw new AssertionError(c + " doesn't properly parameterize " + base); @@ -1422,7 +1421,7 @@ public static String getRelativeNameFrom(@CheckForNull Item p, @CheckForNull Ite StringBuilder buf = new StringBuilder(); Item i = p; while (true) { - if (buf.length() > 0) buf.insert(0, separationString); + if (!buf.isEmpty()) buf.insert(0, separationString); buf.insert(0, useDisplayName ? i.getDisplayName() : i.getName()); ItemGroup gr = i.getParent(); @@ -1873,7 +1872,7 @@ public static String joinPath(String... components) { for (String s : components) { if (s.isEmpty()) continue; - if (buf.length() > 0) { + if (!buf.isEmpty()) { if (buf.charAt(buf.length() - 1) != '/') buf.append('/'); if (s.charAt(0) == '/') s = s.substring(1); @@ -1979,8 +1978,7 @@ public String getServerName() { @Deprecated public String getCheckUrl(String userDefined, Object descriptor, String field) { if (userDefined != null || field == null) return userDefined; - if (descriptor instanceof Descriptor) { - Descriptor d = (Descriptor) descriptor; + if (descriptor instanceof Descriptor d) { return d.getCheckUrl(field); } return null; @@ -1993,8 +1991,7 @@ public String getCheckUrl(String userDefined, Object descriptor, String field) { public void calcCheckUrl(Map attributes, String userDefined, Object descriptor, String field) { if (userDefined != null || field == null) return; - if (descriptor instanceof Descriptor) { - Descriptor d = (Descriptor) descriptor; + if (descriptor instanceof Descriptor d) { CheckMethod m = d.getCheckMethod(field); attributes.put("checkUrl", m.toStemUrl()); attributes.put("checkDependsOn", m.getDependsOn()); @@ -2057,7 +2054,7 @@ public static List> getCloudDescriptors() { * Prepend a prefix only when there's the specified body. */ public String prepend(String prefix, String body) { - if (body != null && body.length() > 0) + if (body != null && !body.isEmpty()) return prefix + body; return body; } diff --git a/core/src/main/java/hudson/PluginFirstClassLoader2.java b/core/src/main/java/hudson/PluginFirstClassLoader2.java index 974939d53acb..30618835c8a7 100644 --- a/core/src/main/java/hudson/PluginFirstClassLoader2.java +++ b/core/src/main/java/hudson/PluginFirstClassLoader2.java @@ -25,8 +25,9 @@ public class PluginFirstClassLoader2 extends URLClassLoader2 { registerAsParallelCapable(); } - public PluginFirstClassLoader2(@NonNull URL[] urls, @NonNull ClassLoader parent) { - super(Objects.requireNonNull(urls), Objects.requireNonNull(parent)); + + public PluginFirstClassLoader2(String name, @NonNull URL[] urls, @NonNull ClassLoader parent) { + super(name, Objects.requireNonNull(urls), Objects.requireNonNull(parent)); } /** diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index d666fc207096..16c22e9ca90c 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -91,6 +91,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -138,10 +139,12 @@ import jenkins.util.xml.RestrictiveEntityResolver; import net.sf.json.JSONArray; import net.sf.json.JSONObject; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload2.core.DiskFileItem; +import org.apache.commons.fileupload2.core.DiskFileItemFactory; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.javax.JavaxServletDiskFileUpload; +import org.apache.commons.fileupload2.javax.JavaxServletFileUpload; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; @@ -612,7 +615,7 @@ public void run(Reactor reactor) throws Exception { }}); } - void considerDetachedPlugin(String shortName) { + void considerDetachedPlugin(String shortName, String source) { if (new File(rootDir, shortName + ".jpi").isFile() || new File(rootDir, shortName + ".hpi").isFile() || new File(rootDir, shortName + ".jpl").isFile() || @@ -624,7 +627,7 @@ void considerDetachedPlugin(String shortName) { for (String loadedFile : loadPluginsFromWar(getDetachedLocation(), (dir, name) -> normalisePluginName(name).equals(shortName))) { String loaded = normalisePluginName(loadedFile); File arc = new File(rootDir, loaded + ".jpi"); - LOGGER.info(() -> "Loading a detached plugin as a dependency: " + arc); + LOGGER.info(() -> "Loading a detached plugin " + arc + " as a dependency of " + source); try { plugins.add(strategy.createPluginWrapper(arc)); } catch (IOException e) { @@ -713,6 +716,10 @@ protected static void addDependencies(URL hpiResUrl, String fromPath, Set d } Manifest manifest = parsePluginManifest(hpiResUrl); + if (manifest == null) { + return; + } + String dependencySpec = manifest.getMainAttributes().getValue("Plugin-Dependencies"); if (dependencySpec != null) { String[] dependencyTokens = dependencySpec.split(","); @@ -1098,7 +1105,7 @@ protected void copyBundledPlugin(URL src, String fileName) throws IOException { } /*package*/ static @CheckForNull Manifest parsePluginManifest(URL bundledJpi) { - try (URLClassLoader cl = new URLClassLoader(new URL[]{bundledJpi})) { + try (URLClassLoader cl = new URLClassLoader("Temporary classloader for parsing " + bundledJpi.toString(), new URL[]{bundledJpi}, ClassLoader.getSystemClassLoader())) { InputStream in = null; try { URL res = cl.findResource(PluginWrapper.MANIFEST_FILENAME); @@ -1253,6 +1260,13 @@ public List getPlugins() { return Collections.unmodifiableList(plugins); } + @Restricted(NoExternalUse.class) // used by jelly + public List getPluginsSortedByTitle() { + return plugins.stream() + .sorted(Comparator.comparing(PluginWrapper::getDisplayName, String.CASE_INSENSITIVE_ORDER)) + .collect(Collectors.toUnmodifiableList()); + } + public List getFailedPlugins() { return failedPlugins; } @@ -1435,13 +1449,13 @@ public HttpResponse doPluginsSearch(@QueryParameter String query, @QueryParamete if (query == null || query.isBlank()) { return true; } - return (plugin.name != null && plugin.name.toLowerCase().contains(query.toLowerCase())) || - (plugin.title != null && plugin.title.toLowerCase().contains(query.toLowerCase())) || - (plugin.excerpt != null && plugin.excerpt.toLowerCase().contains(query.toLowerCase())) || + return (plugin.name != null && plugin.name.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT))) || + (plugin.title != null && plugin.title.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT))) || + (plugin.excerpt != null && plugin.excerpt.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT))) || plugin.hasCategory(query) || plugin.getCategoriesStream() .map(UpdateCenter::getCategoryDisplayName) - .anyMatch(category -> category != null && category.toLowerCase().contains(query.toLowerCase())) || + .anyMatch(category -> category != null && category.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT))) || plugin.hasWarnings() && query.equalsIgnoreCase("warning:"); }) .limit(Math.max(limit - plugins.size(), 1)) @@ -1824,13 +1838,21 @@ static class FileUploadPluginCopier implements PluginCopier { } @Override - public void copy(File target) throws Exception { - fileItem.write(target); + public void copy(File target) throws IOException { + try { + fileItem.write(Util.fileToPath(target)); + } catch (UncheckedIOException e) { + throw e.getCause(); + } } @Override public void cleanup() { - fileItem.delete(); + try { + fileItem.delete(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } } @@ -1865,8 +1887,8 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl String fileName = ""; PluginCopier copier; File tmpDir = Files.createTempDirectory("uploadDir").toFile(); - ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, tmpDir)); - List items = upload.parseRequest(req); + JavaxServletFileUpload upload = new JavaxServletDiskFileUpload(DiskFileItemFactory.builder().setFile(tmpDir).get()); + List items = upload.parseRequest(req); String string = items.get(1).getString(); if (string != null && !string.isBlank()) { // this is a URL deployment @@ -2329,7 +2351,7 @@ public static final class UberClassLoader extends ClassLoader { } public UberClassLoader(List activePlugins) { - super(PluginManager.class.getClassLoader()); + super("UberClassLoader", PluginManager.class.getClassLoader()); this.activePlugins = activePlugins; } @@ -2395,6 +2417,22 @@ public String toString() { // only for debugging purpose return "classLoader " + getClass().getName(); } + + // TODO Remove this once we require post 2024-07 remoting minimum version and deleted ClassLoaderProxy#fetchJar(URL) + @SuppressFBWarnings( + value = "DMI_COLLECTION_OF_URLS", + justification = "All URLs point to local files, so no DNS lookup.") + @Restricted(NoExternalUse.class) + public boolean isPluginJar(URL jarUrl) { + for (PluginWrapper plugin : activePlugins) { + if (plugin.classLoader instanceof URLClassLoader) { + if (Set.of(((URLClassLoader) plugin.classLoader).getURLs()).contains(jarUrl)) { + return true; + } + } + } + return false; + } } @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console") diff --git a/core/src/main/java/hudson/ProxyConfiguration.java b/core/src/main/java/hudson/ProxyConfiguration.java index fa4206521ee6..cc5b30d956dc 100644 --- a/core/src/main/java/hudson/ProxyConfiguration.java +++ b/core/src/main/java/hudson/ProxyConfiguration.java @@ -540,6 +540,34 @@ public FormValidation doCheckPort(@QueryParameter String value) { return FormValidation.ok(); } + /** + * Do check if the provided value is empty or composed of whitespaces. + * If so, return a validation warning. + * + * @param value the value to test + * @return a validation warning iff the provided value is empty or composed of whitespaces. + */ + private static FormValidation checkProxyCredentials(String value) { + value = Util.fixEmptyAndTrim(value); + if (value == null) { + return FormValidation.ok(); + } else { + return FormValidation.warning(Messages.ProxyConfiguration_NonTLSWarning()); + } + } + + @RequirePOST + @Restricted(NoExternalUse.class) + public FormValidation doCheckUserName(@QueryParameter String value) { + return checkProxyCredentials(value); + } + + @RequirePOST + @Restricted(NoExternalUse.class) + public FormValidation doCheckSecretPassword(@QueryParameter String value) { + return checkProxyCredentials(value); + } + @RequirePOST @Restricted(NoExternalUse.class) public FormValidation doValidateProxy( diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java index 4265a6283a2b..9e29b141560c 100644 --- a/core/src/main/java/hudson/cli/CLIAction.java +++ b/core/src/main/java/hudson/cli/CLIAction.java @@ -217,7 +217,7 @@ protected void closed(int statusCode, String reason) { @Override public Object getTarget() { StaplerRequest req = Stapler.getCurrentRequest(); - if (req.getRestOfPath().length() == 0 && "POST".equals(req.getMethod())) { + if (req.getRestOfPath().isEmpty() && "POST".equals(req.getMethod())) { // CLI connection request if ("false".equals(req.getParameter("remoting"))) { throw new PlainCliEndpointResponse(); diff --git a/core/src/main/java/hudson/cli/GroovyshCommand.java b/core/src/main/java/hudson/cli/GroovyshCommand.java index 8bc4d522e254..18cbc6da7513 100644 --- a/core/src/main/java/hudson/cli/GroovyshCommand.java +++ b/core/src/main/java/hudson/cli/GroovyshCommand.java @@ -77,7 +77,7 @@ protected int run() { StringBuilder commandLine = new StringBuilder(); for (String arg : args) { - if (commandLine.length() > 0) { + if (!commandLine.isEmpty()) { commandLine.append(" "); } commandLine.append(arg); diff --git a/core/src/main/java/hudson/console/AnnotatedLargeText.java b/core/src/main/java/hudson/console/AnnotatedLargeText.java index 8e0a59bee2af..4e0d3b9908af 100644 --- a/core/src/main/java/hudson/console/AnnotatedLargeText.java +++ b/core/src/main/java/hudson/console/AnnotatedLargeText.java @@ -28,8 +28,6 @@ import static java.lang.Math.abs; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.CheckReturnValue; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.remoting.ObjectInputStreamEx; @@ -45,6 +43,8 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; diff --git a/core/src/main/java/hudson/console/ConsoleNote.java b/core/src/main/java/hudson/console/ConsoleNote.java index 20ded06e9862..633a5ed54743 100644 --- a/core/src/main/java/hudson/console/ConsoleNote.java +++ b/core/src/main/java/hudson/console/ConsoleNote.java @@ -24,8 +24,6 @@ package hudson.console; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.ExtensionPoint; import hudson.Functions; @@ -50,6 +48,8 @@ import java.util.Base64; import java.util.Collection; import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import jenkins.model.Jenkins; import jenkins.security.HMACConfidentialKey; import jenkins.util.JenkinsJVM; diff --git a/core/src/main/java/hudson/console/ExpandableDetailsNote.java b/core/src/main/java/hudson/console/ExpandableDetailsNote.java index 1f558e6a260b..beb623b5034c 100644 --- a/core/src/main/java/hudson/console/ExpandableDetailsNote.java +++ b/core/src/main/java/hudson/console/ExpandableDetailsNote.java @@ -26,8 +26,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; -import hudson.Functions; import hudson.MarkupText; +import hudson.Util; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,7 +53,8 @@ public ExpandableDetailsNote(String caption, String html) { @Override public ConsoleAnnotator annotate(Object context, MarkupText text, int charPos) { text.addMarkup(charPos, - "
" + html + "
"); + "
" + html + "
"); return null; } diff --git a/core/src/main/java/hudson/console/HyperlinkNote.java b/core/src/main/java/hudson/console/HyperlinkNote.java index 93e2ff40f493..1e582dab2b4a 100644 --- a/core/src/main/java/hudson/console/HyperlinkNote.java +++ b/core/src/main/java/hudson/console/HyperlinkNote.java @@ -88,10 +88,8 @@ static String encodeTo(String url, String text, BiFunction errors) { buf.append(e.getClass().getSimpleName()).append(": ").append(e.getMessage()); } } - if (buf.length() == 0) return; + if (buf.isEmpty()) return; Jenkins j = Jenkins.getInstanceOrNull(); if (j == null) { // Need this path, at least for unit tests, but also in case of very broken startup // Startup failed, something is very broken, so report what we can. diff --git a/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java b/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java index 8873925847dc..c305e9a6febc 100644 --- a/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java +++ b/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java @@ -18,8 +18,8 @@ import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.UncaughtExceptionFilter; import org.kohsuke.stapler.WebApp; -import org.kohsuke.stapler.compression.CompressionFilter; /** * Deals with exceptions that get thrown all the way up to the Stapler rendering layer. @@ -30,7 +30,7 @@ public class InstallUncaughtExceptionHandler { @Initializer public static void init(final Jenkins j) throws IOException { - CompressionFilter.setUncaughtExceptionHandler(j.servletContext, (e, context, req, rsp) -> handleException(j, e, req, rsp, 500)); + UncaughtExceptionFilter.setUncaughtExceptionHandler(j.servletContext, (e, context, req, rsp) -> handleException(j, e, req, rsp, 500)); try { Thread.setDefaultUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler()); LOGGER.log(Level.FINE, "Successfully installed a global UncaughtExceptionHandler."); diff --git a/core/src/main/java/hudson/lifecycle/ExitLifecycle.java b/core/src/main/java/hudson/lifecycle/ExitLifecycle.java index f8fcc3abefbf..038dafc442a3 100644 --- a/core/src/main/java/hudson/lifecycle/ExitLifecycle.java +++ b/core/src/main/java/hudson/lifecycle/ExitLifecycle.java @@ -26,6 +26,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.util.BootFailure; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; @@ -72,4 +73,9 @@ public void restart() { System.exit(exitOnRestart); } + + @Override + public void onBootFailure(BootFailure problem) { + restart(); + } } diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index dbc53d2b5005..028c3d1826b7 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -32,6 +32,8 @@ import hudson.Util; import hudson.init.InitMilestone; import hudson.init.Initializer; +import hudson.util.BootFailure; +import hudson.util.JenkinsReloadFailed; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; @@ -324,6 +326,14 @@ public boolean supportsDynamicLoad() { return true; } + /** + * Called when Jenkins has failed to boot. + * @param problem a boot failure (could be {@link JenkinsReloadFailed}) + * @since 2.469 + */ + public void onBootFailure(BootFailure problem) { + } + @Restricted(NoExternalUse.class) public static final class PlaceholderLifecycle extends ExitLifecycle { diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java index 23ebc72cde6a..86aea6575048 100644 --- a/core/src/main/java/hudson/logging/LogRecorder.java +++ b/core/src/main/java/hudson/logging/LogRecorder.java @@ -393,7 +393,7 @@ private static final class SetLevel extends MasterToSlaveCallable { void broadcast() { for (Computer c : Jenkins.get().getComputers()) { - if (c.getName().length() > 0) { // i.e. not master + if (!c.getName().isEmpty()) { // i.e. not master VirtualChannel ch = c.getChannel(); if (ch != null) { try { @@ -595,7 +595,7 @@ public int compare(Computer c1, Computer c2) { } }); for (Computer c : Jenkins.get().getComputers()) { - if (c.getName().length() == 0) { + if (c.getName().isEmpty()) { continue; // master } List recs = new ArrayList<>(); diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index bcd29082ebe1..85d62df78896 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -607,8 +607,7 @@ protected Launcher createLauncher(@NonNull BuildListener listener) throws IOExce final Node currentNode = getCurrentNode(); Launcher l = currentNode.createLauncher(listener); - if (project instanceof BuildableItemWithBuildWrappers) { - BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; + if (project instanceof BuildableItemWithBuildWrappers biwbw) { for (BuildWrapper bw : biwbw.getBuildWrappersList()) l = bw.decorateLauncher(AbstractBuild.this, l, listener); } diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 0649bdb33b10..adebec8f289f 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -25,7 +25,6 @@ package hudson.model; -import static hudson.model.queue.Executables.getParentOf; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; @@ -39,9 +38,6 @@ import hudson.cli.declarative.CLIResolver; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; -import hudson.model.queue.SubTask; -import hudson.model.queue.Tasks; -import hudson.model.queue.WorkUnit; import hudson.security.ACL; import hudson.security.ACLContext; import hudson.security.AccessControlled; @@ -57,12 +53,8 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; -import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -551,8 +543,7 @@ public final String getUrl() { List ancestors = req.getAncestors(); if (!ancestors.isEmpty()) { Ancestor last = ancestors.get(ancestors.size() - 1); - if (last.getObject() instanceof View) { - View view = (View) last.getObject(); + if (last.getObject() instanceof View view) { if (view.getOwner().getItemGroup() == getParent() && !view.isDefault()) { // Showing something inside a view, so should use that as the base URL. String prefix = req.getContextPath() + "/"; @@ -706,11 +697,13 @@ public void delete(StaplerRequest req, StaplerResponse rsp) throws IOException, * *

* Any exception indicates the deletion has failed, but {@link AbortException} would prevent the caller - * from showing the stack trace. This + * from showing the stack trace. + * @see ItemDeletion */ @Override public void delete() throws IOException, InterruptedException { checkPermission(DELETE); + ItemListener.checkBeforeDelete(this); boolean responsibleForAbortingBuilds = !ItemDeletion.contains(this); boolean ownsRegistration = ItemDeletion.register(this); if (!ownsRegistration && ItemDeletion.isRegistered(this)) { @@ -720,87 +713,7 @@ public void delete() throws IOException, InterruptedException { try { // if a build is in progress. Cancel it. if (responsibleForAbortingBuilds || ownsRegistration) { - Queue queue = Queue.getInstance(); - if (this instanceof Queue.Task) { - // clear any items in the queue so they do not get picked up - queue.cancel((Queue.Task) this); - } - // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot - for (Queue.Item i : queue.getItems()) { - Item item = Tasks.getItemOf(i.task); - while (item != null) { - if (item == this) { - if (!queue.cancel(i)) { - LOGGER.warning(() -> "failed to cancel " + i); - } - break; - } - if (item.getParent() instanceof Item) { - item = (Item) item.getParent(); - } else { - break; - } - } - } - // interrupt any builds in progress (and this should be a recursive test so that folders do not pay - // the 15 second delay for every child item). This happens after queue cancellation, so will be - // a complete set of builds in flight - Map buildsInProgress = new LinkedHashMap<>(); - for (Computer c : Jenkins.get().getComputers()) { - for (Executor e : c.getAllExecutors()) { - final WorkUnit workUnit = e.getCurrentWorkUnit(); - final Queue.Executable executable = workUnit != null ? workUnit.getExecutable() : null; - final SubTask subtask = executable != null ? getParentOf(executable) : null; - - if (subtask != null) { - Item item = Tasks.getItemOf(subtask); - while (item != null) { - if (item == this) { - buildsInProgress.put(e, e.getCurrentExecutable()); - e.interrupt(Result.ABORTED); - break; - } - if (item.getParent() instanceof Item) { - item = (Item) item.getParent(); - } else { - break; - } - } - } - } - } - if (!buildsInProgress.isEmpty()) { - // give them 15 seconds or so to respond to the interrupt - long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); - // comparison with executor.getCurrentExecutable() == computation currently should always be true - // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling - while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) { - // we know that ItemDeletion will prevent any new builds in the queue - // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear - // Queue.cancel happens-before collecting the buildsInProgress list - // thus buildsInProgress contains the complete set we need to interrupt and wait for - for (Iterator> iterator = - buildsInProgress.entrySet().iterator(); - iterator.hasNext(); ) { - Map.Entry entry = iterator.next(); - // comparison with executor.getCurrentExecutable() == executable currently should always be - // true as we no longer recycle Executors, but safer to future-proof in case we ever - // revisit recycling. - if (!entry.getKey().isAlive() - || entry.getValue() != entry.getKey().getCurrentExecutable()) { - iterator.remove(); - } - // I don't know why, but we have to keep interrupting - entry.getKey().interrupt(Result.ABORTED); - } - Thread.sleep(50L); - } - if (!buildsInProgress.isEmpty()) { - throw new Failure(Messages.AbstractItem_FailureToStopBuilds( - buildsInProgress.size(), getFullDisplayName() - )); - } - } + ItemDeletion.cancelBuildsInProgress(this); } if (this instanceof ItemGroup) { // delete individual items first @@ -937,6 +850,7 @@ public void updateByXml(Source source) throws IOException { // if everything went well, commit this new version out.commit(); SaveableListener.fireOnChange(this, getConfigFile()); + ItemListener.fireOnUpdated(this); } finally { out.abort(); // don't leave anything behind diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index cceff9c42b19..917ff1f7b81f 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -77,7 +77,6 @@ import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.DescribableList; import hudson.util.FormValidation; -import hudson.widgets.HistoryWidget; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -519,8 +518,7 @@ private AbstractBuild getBuildForDeprecatedMethods() { Executor e = Executor.currentExecutor(); if (e != null) { Executable exe = e.getCurrentExecutable(); - if (exe instanceof AbstractBuild) { - AbstractBuild b = (AbstractBuild) exe; + if (exe instanceof AbstractBuild b) { if (b.getProject() == this) return b; } @@ -1013,6 +1011,7 @@ public List getActions() { * null if no information is available (for example, * if no build was done yet.) */ + @SuppressWarnings("deprecation") @Override public Node getLastBuiltOn() { // where was it built on? @@ -1721,11 +1720,6 @@ protected SearchIndexBuilder makeSearchIndex() { return getParameterizedJobMixIn().extendSearchIndex(super.makeSearchIndex()); } - @Override - protected HistoryWidget createHistoryWidget() { - return buildMixIn.createHistoryWidget(); - } - // // // actions diff --git a/core/src/main/java/hudson/model/AdministrativeMonitor.java b/core/src/main/java/hudson/model/AdministrativeMonitor.java index 7a9a0dde970c..bdbfb48027d3 100644 --- a/core/src/main/java/hudson/model/AdministrativeMonitor.java +++ b/core/src/main/java/hudson/model/AdministrativeMonitor.java @@ -183,7 +183,7 @@ public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOExceptio /** * Required permission to view this admin monitor. - * By default {@link Jenkins#ADMINISTER}, but {@link Jenkins#SYSTEM_READ} is also supported. + * By default {@link Jenkins#ADMINISTER}, but {@link Jenkins#SYSTEM_READ} or {@link Jenkins#MANAGE} are also supported. *

* Changing this permission check to return {@link Jenkins#SYSTEM_READ} will make the active * administrative monitor appear on {@code manage.jelly} and on the globally visible @@ -191,23 +191,69 @@ public void doDisable(StaplerRequest req, StaplerResponse rsp) throws IOExceptio * {@link #doDisable(StaplerRequest, StaplerResponse)} will still always require Administer permission. *

*

+ * This method only allows for a single permission to be returned. If more complex permission checks are required, + * override {@link #checkRequiredPermission()} and {@link #hasRequiredPermission()} instead. + *

+ *

* Implementers need to ensure that {@code doAct} and other web methods perform necessary permission checks: * Users with System Read permissions are expected to be limited to read-only access. * Form UI elements that change system state, e.g. toggling a feature on or off, need to be hidden from users * lacking Administer permission. *

+ * @since 2.233 + * @deprecated Callers should use {@link #checkRequiredPermission()} or {@link #hasRequiredPermission()}. */ + @Deprecated public Permission getRequiredPermission() { return Jenkins.ADMINISTER; } + /** + * Checks if the current user has the minimum required permission to view this administrative monitor. + *

+ * Subclasses may override this method and {@link #hasRequiredPermission()} instead of {@link #getRequiredPermission()} to perform more complex permission checks, + * for example, checking either {@link Jenkins#MANAGE} or {@link Jenkins#SYSTEM_READ}. + *

+ * @see #getRequiredPermission() + * @see #hasRequiredPermission() + * @since 2.468 + */ + public void checkRequiredPermission() { + Jenkins.get().checkPermission(getRequiredPermission()); + } + + /** + * Checks if the current user has the minimum required permission to view this administrative monitor. + *

+ * Subclasses may override this method and {@link #checkRequiredPermission} instead of {@link #getRequiredPermission()} to perform more complex permission checks, + * for example, checking either {@link Jenkins#MANAGE} or {@link Jenkins#SYSTEM_READ}. + *

+ * @see #getRequiredPermission() + * @see #checkRequiredPermission() + * @since 2.468 + */ + public boolean hasRequiredPermission() { + return Jenkins.get().hasPermission(getRequiredPermission()); + } + + /** + * Checks if the current user has the minimum required permission to view any administrative monitor. + * + * @return true if the current user has the minimum required permission to view any administrative monitor. + * + * @since 2.468 + */ + public static boolean hasPermissionToDisplay() { + return Jenkins.get().hasAnyPermission(Jenkins.SYSTEM_READ, Jenkins.MANAGE); + } + /** * Ensure that URLs in this administrative monitor are only accessible to users with {@link #getRequiredPermission()}. */ @Override @Restricted(NoExternalUse.class) public Object getTarget() { - Jenkins.get().checkPermission(getRequiredPermission()); + checkRequiredPermission(); return this; } diff --git a/core/src/main/java/hudson/model/Api.java b/core/src/main/java/hudson/model/Api.java index 11db76c3581d..23e72072112f 100644 --- a/core/src/main/java/hudson/model/Api.java +++ b/core/src/main/java/hudson/model/Api.java @@ -190,8 +190,7 @@ public void doXml(StaplerRequest req, StaplerResponse rsp, return; } - // switch to gzipped output - try (OutputStream o = rsp.getCompressedOutputStream(req)) { + try (OutputStream o = rsp.getOutputStream()) { if (isSimpleOutput(result)) { // simple output allowed rsp.setContentType("text/plain;charset=UTF-8"); diff --git a/core/src/main/java/hudson/model/AutoCompletionCandidates.java b/core/src/main/java/hudson/model/AutoCompletionCandidates.java index 4e58860ceb89..d2f5f17c255f 100644 --- a/core/src/main/java/hudson/model/AutoCompletionCandidates.java +++ b/core/src/main/java/hudson/model/AutoCompletionCandidates.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import javax.servlet.ServletException; import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpResponse; @@ -172,6 +173,6 @@ private String contextualNameOf(Item i) { } private static boolean startsWithImpl(String str, String prefix, boolean ignoreCase) { - return ignoreCase ? str.toLowerCase().startsWith(prefix.toLowerCase()) : str.startsWith(prefix); + return ignoreCase ? str.toLowerCase(Locale.ROOT).startsWith(prefix.toLowerCase(Locale.ROOT)) : str.startsWith(prefix); } } diff --git a/core/src/main/java/hudson/model/BooleanParameterDefinition.java b/core/src/main/java/hudson/model/BooleanParameterDefinition.java index 5e270ef46da1..6e9db32216fd 100644 --- a/core/src/main/java/hudson/model/BooleanParameterDefinition.java +++ b/core/src/main/java/hudson/model/BooleanParameterDefinition.java @@ -59,8 +59,7 @@ public BooleanParameterDefinition(@NonNull String name, boolean defaultValue, @C @Override public ParameterDefinition copyWithDefaultValue(ParameterValue defaultValue) { - if (defaultValue instanceof BooleanParameterValue) { - BooleanParameterValue value = (BooleanParameterValue) defaultValue; + if (defaultValue instanceof BooleanParameterValue value) { return new BooleanParameterDefinition(getName(), value.value, getDescription()); } else { return this; diff --git a/core/src/main/java/hudson/model/BuildAuthorizationToken.java b/core/src/main/java/hudson/model/BuildAuthorizationToken.java index f101eb3d6e74..a09ed113e1cf 100644 --- a/core/src/main/java/hudson/model/BuildAuthorizationToken.java +++ b/core/src/main/java/hudson/model/BuildAuthorizationToken.java @@ -29,7 +29,6 @@ import hudson.security.ACL; import java.io.IOException; import javax.servlet.http.HttpServletResponse; -import jenkins.security.ApiTokenProperty; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -82,10 +81,6 @@ public static void checkPermission(Job project, BuildAuthorizationToken to return; } - if (req.getAttribute(ApiTokenProperty.class.getName()) instanceof User) { - return; - } - rsp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); rsp.addHeader("Allow", "POST"); throw HttpResponses.forwardToView(project, "requirePOST.jelly"); diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java index 6990dd2b39e0..668587ffc8fc 100644 --- a/core/src/main/java/hudson/model/Cause.java +++ b/core/src/main/java/hudson/model/Cause.java @@ -182,6 +182,10 @@ public UpstreamCause(Run up) { upstreamCauses = new ArrayList<>(); Set traversed = new HashSet<>(); for (Cause c : up.getCauses()) { + if (traversed.size() >= MAX_LEAF) { + upstreamCauses.add(new DeeplyNestedUpstreamCause()); + break; + } upstreamCauses.add(trim(c, MAX_DEPTH, traversed)); } } @@ -239,14 +243,16 @@ public int hashCode() { } UpstreamCause uc = (UpstreamCause) c; List cs = new ArrayList<>(); - if (depth > 0) { - if (traversed.add(uc.upstreamUrl + uc.upstreamBuild)) { - for (Cause c2 : uc.upstreamCauses) { - cs.add(trim(c2, depth - 1, traversed)); + if (traversed.add(uc.upstreamUrl + uc.upstreamBuild)) { + for (Cause c2 : uc.upstreamCauses) { + if (depth <= 0 || traversed.size() >= MAX_LEAF) { + cs.add(new DeeplyNestedUpstreamCause()); + break; } + cs.add(trim(c2, depth - 1, traversed)); } - } else if (traversed.size() < MAX_LEAF) { - cs.add(new DeeplyNestedUpstreamCause()); + } else { + traversed.add(uc.upstreamUrl + uc.upstreamBuild + '#' + traversed.size()); } return new UpstreamCause(uc.upstreamProject, uc.upstreamBuild, uc.upstreamUrl, cs); } diff --git a/core/src/main/java/hudson/model/CauseAction.java b/core/src/main/java/hudson/model/CauseAction.java index eca42ffb6999..316cf3a182d6 100644 --- a/core/src/main/java/hudson/model/CauseAction.java +++ b/core/src/main/java/hudson/model/CauseAction.java @@ -61,8 +61,7 @@ public CauseAction(Cause c) { private void addCause(Cause c) { synchronized (causeBag) { - Integer cnt = causeBag.get(c); - causeBag.put(c, cnt == null ? 1 : cnt + 1); + causeBag.compute(c, (unused, cnt) -> cnt == null ? 1 : cnt + 1); } } diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index 3470ad4d15c4..14a311381a37 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -1457,7 +1457,7 @@ public void doDumpExportTable(StaplerRequest req, StaplerResponse rsp) throws IO checkPermission(Jenkins.ADMINISTER); rsp.setContentType("text/plain"); - try (PrintWriter w = new PrintWriter(rsp.getCompressedWriter(req))) { + try (PrintWriter w = new PrintWriter(rsp.getWriter())) { VirtualChannel vc = getChannel(); if (vc instanceof Channel) { w.println("Controller to agent"); diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java index 817115c3ad17..b5eb07784d27 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -294,8 +294,7 @@ protected Descriptor() { // detect an type error Type bt = Types.getBaseClass(getClass(), Descriptor.class); - if (bt instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) bt; + if (bt instanceof ParameterizedType pt) { // this 't' is the closest approximation of T of Descriptor. Class t = Types.erasure(pt.getActualTypeArguments()[0]); if (!t.isAssignableFrom(clazz)) @@ -595,6 +594,9 @@ public T newInstance(@Nullable StaplerRequest req, @NonNull JSONObject formData) return verifyNewInstance(bindJSON(req, clazz, formData, true)); } } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException | RuntimeException e) { + if (e instanceof RuntimeException && e instanceof HttpResponse) { + throw (RuntimeException) e; + } throw new LinkageError("Failed to instantiate " + clazz + " from " + RedactSecretJsonInErrorMessageSanitizer.INSTANCE.sanitize(formData), e); } } @@ -675,7 +677,7 @@ public Object instantiate(Class actualType, JSONObject json) { + actualType.getName() + " " + json); } } catch (Exception x) { - LOGGER.log(Level.WARNING, "falling back to default instantiation " + actualType.getName() + " " + json, x); + LOGGER.log(x instanceof HttpResponse ? Level.FINE : Level.WARNING, "falling back to default instantiation " + actualType.getName() + " " + json, x); // If nested objects are not using newInstance, bindJSON will wind up throwing the same exception anyway, // so logging above will result in a duplicated stack trace. // However if they *are* then this is the only way to find errors in that newInstance. @@ -688,8 +690,7 @@ public Object instantiate(Class actualType, JSONObject json) { @Override public Object onConvert(Type targetType, Class targetTypeErasure, Object jsonSource) { - if (jsonSource instanceof JSONObject) { - JSONObject json = (JSONObject) jsonSource; + if (jsonSource instanceof JSONObject json) { if (isApplicable(targetTypeErasure, json)) { LOGGER.log(Level.FINE, "switching to newInstance {0} {1}", new Object[] {targetTypeErasure.getName(), json}); try { @@ -895,8 +896,7 @@ protected final String getViewPage(Class clazz, String pageName) { protected List getPossibleViewNames(String baseName) { List names = new ArrayList<>(); for (Facet f : WebApp.get(Jenkins.get().servletContext).facets) { - if (f instanceof JellyCompatibleFacet) { - JellyCompatibleFacet jcf = (JellyCompatibleFacet) f; + if (f instanceof JellyCompatibleFacet jcf) { for (String ext : jcf.getScriptExtensions()) names.add(baseName + ext); } @@ -1007,6 +1007,10 @@ public static URL getStaticHelpUrl(StaplerRequest req, Klass c, String suffix if (url != null) return url; url = c.getResource(base + '_' + locale.getLanguage() + ".html"); if (url != null) return url; + if (locale.getLanguage().equals("en")) { + url = c.getResource(base + ".html"); + if (url != null) return url; + } } // default diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java index 8083efa72dbf..9d455f4c1bd3 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -33,6 +33,7 @@ import java.io.OutputStream; import java.io.Serializable; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.LinkOption; import java.nio.file.OpenOption; @@ -90,11 +91,6 @@ public final class DirectoryBrowserSupport implements HttpResponse { private static final Pattern TMPDIR_PATTERN = Pattern.compile(".+@tmp/.*"); - /** - * Escape hatch for the protection against SECURITY-2481. If enabled, the absolute paths on Windows will be allowed. - */ - static final String ALLOW_ABSOLUTE_PATH_PROPERTY_NAME = DirectoryBrowserSupport.class.getName() + ".allowAbsolutePath"; - public final ModelObject owner; public final String title; @@ -230,7 +226,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root String pathElement = pathTokens.nextToken(); // Treat * and ? as wildcard unless they match a literal filename if ((pathElement.contains("?") || pathElement.contains("*")) - && inBase && !root.child((_base.length() > 0 ? _base + "/" : "") + pathElement).exists()) + && inBase && !root.child((!_base.isEmpty() ? _base + "/" : "") + pathElement).exists()) inBase = false; if (pathElement.equals("*zip*")) { // the expected syntax is foo/bar/*zip*/bar.zip @@ -245,7 +241,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root } StringBuilder sb = inBase ? _base : _rest; - if (sb.length() > 0) sb.append('/'); + if (!sb.isEmpty()) sb.append('/'); sb.append(pathElement); if (!inBase) restSize++; @@ -260,13 +256,11 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root if (base.isEmpty()) { baseFile = root; } else { - if (!SystemProperties.getBoolean(ALLOW_ABSOLUTE_PATH_PROPERTY_NAME, false)) { - boolean isAbsolute = root.run(new IsAbsolute(base)); - if (isAbsolute) { - LOGGER.info(() -> "SECURITY-2481 The path provided in the URL (" + base + ") is absolute and thus is refused."); - rsp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } + boolean isAbsolute = root.run(new IsAbsolute(base)); + if (isAbsolute) { + LOGGER.info(() -> "SECURITY-2481 The path provided in the URL (" + base + ") is absolute and thus is refused."); + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; } baseFile = root.child(base); } @@ -315,7 +309,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root } List> glob = null; - boolean patternUsed = rest.length() > 0; + boolean patternUsed = !rest.isEmpty(); boolean containsSymlink = false; boolean containsTmpDir = false; if (patternUsed) { @@ -530,7 +524,7 @@ private static String createBackRef(int times) { private static void zip(StaplerResponse rsp, VirtualFile root, VirtualFile dir, String glob) throws IOException, InterruptedException { OutputStream outputStream = rsp.getOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { - zos.setEncoding(System.getProperty("file.encoding")); // TODO JENKINS-20663 make this overridable via query parameter + zos.setEncoding(Charset.defaultCharset().displayName()); // TODO JENKINS-20663 make this overridable via query parameter // TODO consider using run(Callable) here if (glob.isEmpty()) { diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java index b9a5610c4e35..0988b36785dd 100644 --- a/core/src/main/java/hudson/model/DownloadService.java +++ b/core/src/main/java/hudson/model/DownloadService.java @@ -404,7 +404,7 @@ public FormValidation updateNow() throws IOException { } jsonList.add(o); } - if (jsonList.size() == 0 && toolInstallerMetadataExists) { + if (jsonList.isEmpty() && toolInstallerMetadataExists) { return FormValidation.warning("None of the tool installer metadata passed the signature check"); } else if (!toolInstallerMetadataExists) { LOGGER.log(Level.WARNING, "No tool installer metadata found for " + id); diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index 0b40cc60eb18..020399bdd5b8 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -324,17 +324,19 @@ private void resetWorkUnit(String reason) { @Override public void run() { - if (!owner.isOnline()) { - resetWorkUnit("went off-line before the task's worker thread started"); - owner.removeExecutor(this); - queue.scheduleMaintenance(); - return; - } - if (owner.getNode() == null) { - resetWorkUnit("was removed before the task's worker thread started"); - owner.removeExecutor(this); - queue.scheduleMaintenance(); - return; + if (!(owner instanceof Jenkins.MasterComputer)) { + if (!owner.isOnline()) { + resetWorkUnit("went off-line before the task's worker thread started"); + owner.removeExecutor(this); + queue.scheduleMaintenance(); + return; + } + if (owner.getNode() == null) { + resetWorkUnit("was removed before the task's worker thread started"); + owner.removeExecutor(this); + queue.scheduleMaintenance(); + return; + } } final WorkUnit workUnit; lock.writeLock().lock(); @@ -352,13 +354,15 @@ public void run() { task = Queue.withLock(new Callable<>() { @Override public SubTask call() throws Exception { - if (!owner.isOnline()) { - resetWorkUnit("went off-line before the task's worker thread was ready to execute"); - return null; - } - if (owner.getNode() == null) { - resetWorkUnit("was removed before the task's worker thread was ready to execute"); - return null; + if (!(owner instanceof Jenkins.MasterComputer)) { + if (!owner.isOnline()) { + resetWorkUnit("went off-line before the task's worker thread was ready to execute"); + return null; + } + if (owner.getNode() == null) { + resetWorkUnit("was removed before the task's worker thread was ready to execute"); + return null; + } } // after this point we cannot unwind the assignment of the work unit, if the owner // is removed or goes off-line then the build will just have to fail. diff --git a/core/src/main/java/hudson/model/ExecutorListener.java b/core/src/main/java/hudson/model/ExecutorListener.java index 6500e6acc2ec..beff34804d92 100644 --- a/core/src/main/java/hudson/model/ExecutorListener.java +++ b/core/src/main/java/hudson/model/ExecutorListener.java @@ -30,7 +30,7 @@ /** * A listener for task related events from executors. * A {@link Computer#getRetentionStrategy} or {@link SlaveComputer#getLauncher} may implement this interface. - * Or you may create an implementation as an extension (since TODO). + * Or you may create an implementation as an extension (since 2.318). * @author Stephen Connolly * @since 1.312 */ diff --git a/core/src/main/java/hudson/model/FileParameterDefinition.java b/core/src/main/java/hudson/model/FileParameterDefinition.java index 343dd5831203..25cb08336da3 100644 --- a/core/src/main/java/hudson/model/FileParameterDefinition.java +++ b/core/src/main/java/hudson/model/FileParameterDefinition.java @@ -35,7 +35,7 @@ import java.util.Objects; import javax.servlet.ServletException; import net.sf.json.JSONObject; -import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload2.core.FileItem; import org.apache.commons.io.FileUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; @@ -90,7 +90,7 @@ public String getHelpFile() { public ParameterValue createValue(StaplerRequest req) { FileItem src; try { - src = req.getFileItem(getName()); + src = req.getFileItem2(getName()); } catch (ServletException | IOException e) { // Not sure what to do here. We might want to raise this // but that would involve changing the throws for this call diff --git a/core/src/main/java/hudson/model/FileParameterValue.java b/core/src/main/java/hudson/model/FileParameterValue.java index e9e30e63463b..343e30bb64f4 100644 --- a/core/src/main/java/hudson/model/FileParameterValue.java +++ b/core/src/main/java/hudson/model/FileParameterValue.java @@ -39,12 +39,13 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.Path; import java.util.regex.Pattern; import jenkins.util.SystemProperties; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileItemHeaders; -import org.apache.commons.fileupload.disk.DiskFileItem; -import org.apache.commons.fileupload.util.FileItemHeadersImpl; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileItemFactory; +import org.apache.commons.fileupload2.core.FileItemHeaders; +import org.apache.commons.fileupload2.core.FileItemHeadersProvider; import org.apache.commons.io.FilenameUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -55,12 +56,6 @@ /** * {@link ParameterValue} for {@link FileParameterDefinition}. * - *

Persistence

- *

- * {@link DiskFileItem} is persistable via serialization, - * (although the data may get very large in XML) so this object - * as a whole is persistable. - * * @author Kohsuke Kawaguchi */ public class FileParameterValue extends ParameterValue { @@ -97,8 +92,16 @@ public FileParameterValue(String name, FileItem file) { this(name, file, FilenameUtils.getName(file.getName())); } + /** + * @deprecated use {@link #FileParameterValue(String, FileItem)} + */ + @Deprecated + public FileParameterValue(String name, org.apache.commons.fileupload.FileItem file) { + this(name, file.toFileUpload2FileItem(), FilenameUtils.getName(file.getName())); + } + public FileParameterValue(String name, File file, String originalFileName) { - this(name, new FileItemImpl(file), originalFileName); + this(name, new FileItemImpl2(file), originalFileName); } protected FileParameterValue(String name, FileItem file, String originalFileName) { @@ -146,10 +149,18 @@ public String getOriginalFileName() { return originalFileName; } - public FileItem getFile() { + public FileItem getFile2() { return file; } + /** + * @deprecated use {@link #getFile2} + */ + @Deprecated + public org.apache.commons.fileupload.FileItem getFile() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(getFile2()); + } + @Override public BuildWrapper createBuildWrapper(AbstractBuild build) { return new BuildWrapper() { @@ -248,11 +259,120 @@ private File getFileParameterFolderUnderBuild(AbstractBuild build) { /** * Default implementation from {@link File}. + * + * @deprecated use {@link FileItemImpl2} */ - public static final class FileItemImpl implements FileItem { - private final File file; + @Deprecated + public static final class FileItemImpl implements org.apache.commons.fileupload.FileItem { + private final FileItem delegate; public FileItemImpl(File file) { + if (file == null) { + throw new NullPointerException("file"); + } + this.delegate = new FileItemImpl2(file); + } + + @Override + public InputStream getInputStream() throws IOException { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getInputStream(); + } + + @Override + public String getContentType() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getContentType(); + } + + @Override + @SuppressFBWarnings(value = "FILE_UPLOAD_FILENAME", justification = "for compatibility") + public String getName() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getName(); + } + + @Override + public boolean isInMemory() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).isInMemory(); + } + + @Override + public long getSize() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getSize(); + } + + @Override + public byte[] get() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).get(); + } + + @Override + public String getString(String encoding) throws UnsupportedEncodingException { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getString(encoding); + } + + @Override + public String getString() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getString(); + } + + @Override + public void write(File to) throws Exception { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).write(to); + } + + @Override + public void delete() { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).delete(); + } + + @Override + public String getFieldName() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getFieldName(); + } + + @Override + public void setFieldName(String name) { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).setFieldName(name); + } + + @Override + public boolean isFormField() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).isFormField(); + } + + @Override + public void setFormField(boolean state) { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).setFormField(state); + } + + @Override + @Deprecated + public OutputStream getOutputStream() throws IOException { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getOutputStream(); + } + + @Override + public org.apache.commons.fileupload.FileItemHeaders getHeaders() { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).getHeaders(); + } + + @Override + public void setHeaders(org.apache.commons.fileupload.FileItemHeaders headers) { + org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(delegate).setHeaders(headers); + } + + @Override + public FileItem toFileUpload2FileItem() { + return delegate; + } + } + + /** + * Default implementation from {@link File}. + */ + public static final class FileItemImpl2 implements FileItem { + private final File file; + + public FileItemImpl2(File file) { if (file == null) { throw new NullPointerException("file"); } @@ -294,8 +414,12 @@ public byte[] get() { } @Override - public String getString(String encoding) throws UnsupportedEncodingException { - return new String(get(), encoding); + public String getString(Charset toCharset) throws IOException { + try { + return new String(get(), toCharset); + } catch (UncheckedIOException e) { + throw e.getCause(); + } } @Override @@ -304,17 +428,19 @@ public String getString() { } @Override - public void write(File to) throws Exception { - new FilePath(file).copyTo(new FilePath(to)); + public FileItem write(Path to) throws IOException { + try { + new FilePath(file).copyTo(new FilePath(to.toFile())); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return this; } @Override - public void delete() { - try { - Files.deleteIfExists(file.toPath()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + public FileItem delete() throws IOException { + Files.deleteIfExists(Util.fileToPath(file)); + return this; } @Override @@ -323,7 +449,8 @@ public String getFieldName() { } @Override - public void setFieldName(String name) { + public FileItem setFieldName(String name) { + return this; } @Override @@ -332,7 +459,8 @@ public boolean isFormField() { } @Override - public void setFormField(boolean state) { + public FileItem setFormField(boolean state) { + return this; } @Override @@ -343,11 +471,12 @@ public OutputStream getOutputStream() throws IOException { @Override public FileItemHeaders getHeaders() { - return new FileItemHeadersImpl(); + return FileItemFactory.AbstractFileItemBuilder.newFileItemHeaders(); } @Override - public void setHeaders(FileItemHeaders headers) { + public FileItemHeadersProvider setHeaders(FileItemHeaders headers) { + return this; } } } diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java index bd7721906b5f..df482e28e03c 100644 --- a/core/src/main/java/hudson/model/Fingerprint.java +++ b/core/src/main/java/hudson/model/Fingerprint.java @@ -608,7 +608,7 @@ public synchronized boolean removeAll(RangeSet that) { public synchronized String toString() { StringBuilder buf = new StringBuilder(); for (Range r : ranges) { - if (buf.length() > 0) buf.append(','); + if (!buf.isEmpty()) buf.append(','); buf.append(r); } return buf.toString(); @@ -787,7 +787,7 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC public static String serialize(RangeSet src) { StringBuilder buf = new StringBuilder(src.ranges.size() * 10); for (Range r : src.ranges) { - if (buf.length() > 0) buf.append(','); + if (!buf.isEmpty()) buf.append(','); if (r.isSingle()) buf.append(r.start); else diff --git a/core/src/main/java/hudson/model/FreeStyleProject.java b/core/src/main/java/hudson/model/FreeStyleProject.java index c63a339d6611..5278d2a301bb 100644 --- a/core/src/main/java/hudson/model/FreeStyleProject.java +++ b/core/src/main/java/hudson/model/FreeStyleProject.java @@ -111,7 +111,7 @@ public String getIconFilePathPattern() { @Override public String getIconClassName() { - return "icon-freestyle-project"; + return "symbol-freestyle-project"; } static { diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java index 2a6b264db9de..faa214dee621 100644 --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java @@ -290,6 +290,7 @@ public synchronized TopLevelItem createProjectFromXML(String name, InputStream x add(result); + result.onCreatedFromScratch(); ItemListener.fireOnCreated(result); Jenkins.get().rebuildDependencyGraphAsync(); diff --git a/core/src/main/java/hudson/model/Items.java b/core/src/main/java/hudson/model/Items.java index a1386565cfd0..fac4ae8a4a01 100644 --- a/core/src/main/java/hudson/model/Items.java +++ b/core/src/main/java/hudson/model/Items.java @@ -208,7 +208,7 @@ public static TopLevelItemDescriptor getDescriptor(String fqcn) { public static String toNameList(Collection items) { StringBuilder buf = new StringBuilder(); for (Item item : items) { - if (buf.length() > 0) + if (!buf.isEmpty()) buf.append(", "); buf.append(item.getFullName()); } diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index 8c4e3183100a..4bd28ff2e325 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -266,8 +266,7 @@ public void onCopied(Item src, Item item) { // If any of the other ItemListeners modify the job, they effect // a save, which will clear the holdOffBuildUntilUserSave and // causing a regression of JENKINS-2494 - if (item instanceof Job) { - Job job = (Job) item; + if (item instanceof Job job) { synchronized (job) { job.holdOffBuildUntilUserSave = false; } @@ -626,11 +625,11 @@ public Collection getOverrides() { } /** - * @deprecated see {@link LazyBuildMixIn#createHistoryWidget()} + * @deprecated Remove any override, history widget is now created via {@link jenkins.widgets.WidgetFactory} implementation. */ @Deprecated(forRemoval = true, since = "2.410") protected HistoryWidget createHistoryWidget() { - return new HistoryWidget(this, getBuilds(), HISTORY_ADAPTER); + throw new IllegalStateException("HistoryWidget is now created via WidgetFactory implementation"); } public static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter<>() { @@ -1110,8 +1109,7 @@ class FeedItem { List entries = new ArrayList<>(); String scmDisplayName = ""; - if (this instanceof SCMTriggerItem) { - SCMTriggerItem scmItem = (SCMTriggerItem) this; + if (this instanceof SCMTriggerItem scmItem) { List scmNames = new ArrayList<>(); for (SCM s : scmItem.getSCMs()) { scmNames.add(s.getDescriptor().getDisplayName()); diff --git a/core/src/main/java/hudson/model/Label.java b/core/src/main/java/hudson/model/Label.java index b0c32f212a09..88661c6df690 100644 --- a/core/src/main/java/hudson/model/Label.java +++ b/core/src/main/java/hudson/model/Label.java @@ -454,7 +454,7 @@ public Api getApi() { public abstract V accept(LabelVisitor visitor, P param); /** - * Lists up all the atoms contained in in this label. + * Lists all the atoms contained in this label. * * @since 1.420 */ @@ -591,7 +591,7 @@ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingCont public static Set parse(@CheckForNull String labels) { final Set r = new TreeSet<>(); labels = fixNull(labels); - if (labels.length() > 0) { + if (!labels.isEmpty()) { Jenkins j = Jenkins.get(); LabelAtom labelAtom = j.tryGetLabelAtom(labels); if (labelAtom == null) { diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java index 77852e994ece..d8f6a8d48bcb 100644 --- a/core/src/main/java/hudson/model/ListView.java +++ b/core/src/main/java/hudson/model/ListView.java @@ -359,7 +359,7 @@ public boolean isAddToCurrentView() { private boolean needToAddToCurrentView(StaplerRequest req) throws ServletException { String json = req.getParameter("json"); - if (json != null && json.length() > 0) { + if (json != null && !json.isEmpty()) { // Submitted via UI JSONObject form = req.getSubmittedForm(); return form.has("addToCurrentView") && form.getBoolean("addToCurrentView"); diff --git a/core/src/main/java/hudson/model/MyViewsProperty.java b/core/src/main/java/hudson/model/MyViewsProperty.java index 9d7b8b651d1c..49fdfac48d2d 100644 --- a/core/src/main/java/hudson/model/MyViewsProperty.java +++ b/core/src/main/java/hudson/model/MyViewsProperty.java @@ -26,9 +26,11 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Util; import hudson.model.Descriptor.FormException; +import hudson.model.userproperty.UserPropertyCategory; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.views.MyViewsTabBar; @@ -41,6 +43,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import javax.servlet.ServletException; import jenkins.model.Jenkins; +import jenkins.util.SystemProperties; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; @@ -50,6 +53,7 @@ import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerFallback; +import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.verb.POST; @@ -59,7 +63,14 @@ * * @author Tom Huybrechts */ -public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback { +public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback, StaplerProxy { + + /** + * Escape hatch for StaplerProxy-based access control + */ + @Restricted(NoExternalUse.class) + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console") + public static /* non-final */ boolean SKIP_PERMISSION_CHECK = SystemProperties.getBoolean(MyViewsProperty.class.getName() + ".skipPermissionCheck"); /** * Name of the primary view defined by the user. @@ -225,7 +236,10 @@ public String getDisplayName() { @Override public String getIconFileName() { - return "symbol-browsers"; + if (SKIP_PERMISSION_CHECK || getACL().hasPermission(Jenkins.ADMINISTER)) + return "symbol-browsers"; + else + return null; } @Override @@ -233,6 +247,14 @@ public String getUrlName() { return "my-views"; } + @Override + public Object getTarget() { + if (!SKIP_PERMISSION_CHECK) { + checkPermission(Jenkins.ADMINISTER); + } + return this; + } + @Extension @Symbol("myView") public static class DescriptorImpl extends UserPropertyDescriptor { @@ -246,6 +268,11 @@ public String getDisplayName() { public UserProperty newInstance(User user) { return new MyViewsProperty(); } + + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Preferences.class); + } } @Override diff --git a/core/src/main/java/hudson/model/PaneStatusProperties.java b/core/src/main/java/hudson/model/PaneStatusProperties.java index 29a460349440..4807020ca714 100644 --- a/core/src/main/java/hudson/model/PaneStatusProperties.java +++ b/core/src/main/java/hudson/model/PaneStatusProperties.java @@ -2,7 +2,9 @@ import static java.lang.String.format; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; +import hudson.model.userproperty.UserPropertyCategory; import hudson.util.PersistedList; import java.io.IOException; import javax.servlet.http.HttpSession; @@ -56,6 +58,10 @@ public boolean isEnabled() { return false; } + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Invisible.class); + } } private static class PaneStatusPropertiesSessionFallback extends PaneStatusProperties { diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index d489f042100d..0d299fb9426d 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -130,6 +130,7 @@ import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -2414,6 +2415,10 @@ public Api getApi() throws AccessDeniedException { } } + public HttpResponse doIndex(StaplerRequest req) { + return HttpResponses.text("Queue item exists. For details check, for example, " + req.getRequestURI() + "api/json?tree=cancelled,executable[url]"); + } + protected Object readResolve() { this.future = new FutureImpl(task); return this; diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index a2061b162b0b..33658252b922 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -33,7 +33,6 @@ import static java.util.logging.Level.SEVERE; import static java.util.logging.Level.WARNING; -import com.jcraft.jzlib.GZIPInputStream; import com.thoughtworks.xstream.XStream; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; @@ -106,6 +105,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import jenkins.model.ArtifactManager; @@ -1341,7 +1341,7 @@ public void computeDisplayName() { private String combineLast(String[] token, int n) { StringBuilder buf = new StringBuilder(); for (int i = Math.max(0, token.length - n); i < token.length; i++) { - if (buf.length() > 0) buf.append('/'); + if (!buf.isEmpty()) buf.append('/'); buf.append(token[i]); } return buf.toString(); @@ -2286,7 +2286,7 @@ public void doBuildTimestamp(StaplerRequest req, StaplerResponse rsp, @QueryPara 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); + OutputStream os = rsp.getOutputStream(); PlainTextConsoleOutputStream out = new PlainTextConsoleOutputStream(os)) { IOUtils.copy(input, out); } diff --git a/core/src/main/java/hudson/model/TimeZoneProperty.java b/core/src/main/java/hudson/model/TimeZoneProperty.java index bdf39c58527e..2675448dad80 100644 --- a/core/src/main/java/hudson/model/TimeZoneProperty.java +++ b/core/src/main/java/hudson/model/TimeZoneProperty.java @@ -4,6 +4,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; +import hudson.model.userproperty.UserPropertyCategory; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.ListBoxModel.Option; @@ -106,6 +107,10 @@ public FormValidation doCheckTimeZoneName(@QueryParameter String timeZoneName) { } } + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Account.class); + } } @CheckForNull diff --git a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java index 0ba59421b0d9..d84e4a684f22 100644 --- a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java +++ b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java @@ -244,7 +244,7 @@ public String getIconClassName() { // this one is easy... too easy... also will never happen return IconSet.toNormalizedIconNameClass(path); } - if (Jenkins.RESOURCE_PATH.length() > 0 && path.startsWith(Jenkins.RESOURCE_PATH)) { + if (!Jenkins.RESOURCE_PATH.isEmpty() && path.startsWith(Jenkins.RESOURCE_PATH)) { // will to live falling path = path.substring(Jenkins.RESOURCE_PATH.length()); } diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 56fd6e77c988..4df95b7b3f45 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -58,14 +58,18 @@ import hudson.util.PersistedList; import hudson.util.VersionNumber; import hudson.util.XStream2; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; @@ -86,6 +90,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.UUID; @@ -390,7 +395,7 @@ public Badge getBadge() { if (size > 0) { StringBuilder tooltip = new StringBuilder(); Badge.Severity severity = Badge.Severity.WARNING; - int securityFixSize = (int) plugins.stream().filter(plugin -> plugin.fixesSecurityVulnerabilities()).count(); + int securityFixSize = (int) plugins.stream().filter(Plugin::fixesSecurityVulnerabilities).count(); int incompatibleSize = (int) plugins.stream().filter(plugin -> !plugin.isCompatibleWithInstalledVersion()).count(); if (size > 1) { tooltip.append(jenkins.management.Messages.PluginsLink_updatesAvailable(size)); @@ -1322,6 +1327,10 @@ public File download(DownloadJob job, URL src) throws IOException { sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out; InputStream in = con.getInputStream(); CountingInputStream cin = new CountingInputStream(in)) { + if (LOGGER.isLoggable(Level.FINE)) { + var sourceUrlString = getSourceUrl(src, con); + LOGGER.fine(() -> "Downloading " + job.getName() + " from " + sourceUrlString); + } while ((len = cin.read(buf)) >= 0) { out.write(buf, 0, len); final int count = cin.getCount(); @@ -1358,15 +1367,22 @@ public File download(DownloadJob job, URL src) throws IOException { return tmp; } catch (IOException e) { // assist troubleshooting in case of e.g. "too many redirects" by printing actual URL - String extraMessage = ""; - if (con != null && con.getURL() != null && !src.toString().equals(con.getURL().toString())) { - // Two URLs are considered equal if different hosts resolve to same IP. Prefer to log in case of string inequality, - // because who knows how the server responds to different host name in the request header? - // Also, since it involved name resolution, it'd be an expensive operation. - extraMessage = " (redirected to: " + con.getURL() + ")"; + throw new IOException("Failed to download from " + getSourceUrl(src, con), e); + } + } + + private static String getSourceUrl(@NonNull URL src, @CheckForNull URLConnection connection) { + var sourceUrlString = src.toExternalForm(); + if (connection != null) { + var connectionURL = connection.getURL(); + if (connectionURL != null) { + var finalUrlString = connectionURL.toExternalForm(); + if (!sourceUrlString.equals(finalUrlString)) { + return sourceUrlString + " → " + finalUrlString; + } } - throw new IOException("Failed to download from " + src + extraMessage, e); } + return sourceUrlString; } /** @@ -1798,6 +1814,83 @@ public void run() { String getComputedSHA512(); } + @SuppressFBWarnings(value = "WEAK_MESSAGE_DIGEST_SHA1", justification = "SHA-1 is only used as a fallback if SHA-256/SHA-512 are not available") + private static class FileWithComputedChecksums implements WithComputedChecksums { + + private final File file; + + private String computedSHA1; + private String computedSHA256; + private String computedSHA512; + + FileWithComputedChecksums(File file) { + this.file = Objects.requireNonNull(file); + } + + @Override + public synchronized String getComputedSHA1() { + if (computedSHA1 != null) { + return computedSHA1; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA1 = computeDigest(messageDigest); + return computedSHA1; + } + + @Override + public synchronized String getComputedSHA256() { + if (computedSHA256 != null) { + return computedSHA256; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA256 = computeDigest(messageDigest); + return computedSHA256; + } + + @Override + public synchronized String getComputedSHA512() { + if (computedSHA512 != null) { + return computedSHA512; + } + + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } + computedSHA512 = computeDigest(messageDigest); + return computedSHA512; + } + + private String computeDigest(MessageDigest digest) { + try (InputStream is = new FileInputStream(file); + BufferedInputStream bis = new BufferedInputStream(is)) { + byte[] buffer = new byte[1024]; + int read = bis.read(buffer, 0, buffer.length); + while (read > -1) { + digest.update(buffer, 0, read); + read = bis.read(buffer, 0, buffer.length); + } + return Base64.getEncoder().encodeToString(digest.digest()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + /** * Base class for a job that downloads a file from the Jenkins project. */ @@ -2234,7 +2327,24 @@ public void _run() throws IOException, InstallationStatus { return; } try { - super._run(); + File cached = getCached(this); + if (cached != null) { + File dst = getDestination(); + + // A bit naive, but following the corresponding logic in UpdateCenterConfiguration#download... + File tmp = new File(dst.getPath() + ".tmp"); + Files.copy(cached.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING); + + config.postValidate(this, tmp); + + /* + * Will unfortunately validate the checksum a second time, but this should still be faster than + * network I/O and at least allows us to reuse code... + */ + config.install(this, tmp, dst); + } else { + super._run(); + } // if this is a bundled plugin, make sure it won't get overwritten PluginWrapper pw = plugin.getInstalled(); @@ -2267,6 +2377,62 @@ public void _run() throws IOException, InstallationStatus { } } + /** + * If we happen to have the file already in the {@coode WEB-INF/detached-plugins} directory and it happens to + * match the checksum we were expecting, then save ourselves a trip to the download site. This method is + * best-effort, and if anything goes wrong we simply fall back to the standard download path. + * + * @return The cached file, or null for a cache miss + */ + @CheckForNull + private File getCached(DownloadJob job) { + URL src; + try { + /* + * Could make PluginManager#getDetachedLocation public and consume it here, but this method is + * best-effort anyway. + */ + src = Jenkins.get().servletContext.getResource(String.format("/WEB-INF/detached-plugins/%s.hpi", plugin.name)); + } catch (MalformedURLException e) { + return null; + } + + if (src == null || !"file".equals(src.getProtocol())) { + return null; + } + + try { + config.preValidate(this, src); + } catch (IOException e) { + return null; + } + + File cached; + try { + cached = new File(src.toURI()); + } catch (URISyntaxException e) { + return null; + } + + if (!cached.isFile()) { + return null; + } + + WithComputedChecksums withComputedChecksums = new FileWithComputedChecksums(cached); + try { + verifyChecksums(withComputedChecksums, plugin, cached); + } catch (IOException | UncheckedIOException | UnsupportedOperationException e) { + return null; + } + + // Allow us to reuse UpdateCenter.InstallationJob#replace. + job.computedSHA1 = withComputedChecksums.getComputedSHA1(); + job.computedSHA256 = withComputedChecksums.getComputedSHA256(); + job.computedSHA512 = withComputedChecksums.getComputedSHA512(); + + return cached; + } + /** * Indicates there is another installation job for this plugin * @since 2.1 diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 7488a9c85b57..8a38ef2d470b 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -192,7 +192,7 @@ public long getDataTimestamp() { @Deprecated public @CheckForNull Future updateDirectly(final boolean signatureCheck) { if (! getDataFile().exists() || isDue()) { - return Jenkins.get().getUpdateCenter().updateService.submit(new Callable() { + return Jenkins.get().getUpdateCenter().updateService.submit(new Callable<>() { @Override public FormValidation call() throws Exception { return updateDirectlyNow(signatureCheck); } diff --git a/core/src/main/java/hudson/model/UsageStatistics.java b/core/src/main/java/hudson/model/UsageStatistics.java index 9fc052cd1009..341f135c52f1 100644 --- a/core/src/main/java/hudson/model/UsageStatistics.java +++ b/core/src/main/java/hudson/model/UsageStatistics.java @@ -26,7 +26,6 @@ import static java.util.concurrent.TimeUnit.DAYS; -import com.jcraft.jzlib.GZIPOutputStream; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; @@ -56,6 +55,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index a242385cb11a..792622eb3c54 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -39,13 +39,11 @@ import hudson.XmlFile; import hudson.init.InitMilestone; import hudson.init.Initializer; -import hudson.model.Descriptor.FormException; import hudson.model.listeners.SaveableListener; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.SecurityRealm; import hudson.security.UserMayOrMayNotExistException2; -import hudson.util.FormApply; import hudson.util.FormValidation; import hudson.util.RunList; import hudson.util.XStream2; @@ -77,7 +75,6 @@ import jenkins.security.LastGrantedAuthoritiesProperty; import jenkins.security.UserDetailsCache; import jenkins.util.SystemProperties; -import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -87,7 +84,6 @@ import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; -import org.kohsuke.stapler.verb.POST; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -342,6 +338,29 @@ public synchronized void addProperty(@NonNull UserProperty p) throws IOException save(); } + /** + * Expand {@link #addProperty(UserProperty)} for multiple properties to be done at once. + * Expected to be used by the categorized configuration pages to update part of the properties. + * The properties not included in the list will be let untouched. + * It will call the {@link UserProperty#setUser(User)} method and at the end, {@link #save()} once. + * + * @since 2.468 + */ + public synchronized void addProperties(@NonNull List multipleProperties) throws IOException { + List newProperties = new ArrayList<>(this.properties); + for (UserProperty property : multipleProperties) { + UserProperty oldProp = getProperty(property.getClass()); + if (oldProp != null) { + newProperties.remove(oldProp); + } + newProperties.add(property); + property.setUser(this); + } + + this.properties = newProperties; + this.save(); + } + /** * List of all {@link UserProperty}s exposed primarily for the remoting API. */ @@ -859,48 +878,6 @@ public Api getApi() { return new Api(this); } - /** - * Accepts submission from the configuration page. - */ - @POST - public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { - checkPermission(Jenkins.ADMINISTER); - - JSONObject json = req.getSubmittedForm(); - String oldFullName = this.fullName; - fullName = json.getString("fullName"); - description = json.getString("description"); - - List props = new ArrayList<>(); - int i = 0; - for (UserPropertyDescriptor d : UserProperty.all()) { - UserProperty p = getProperty(d.clazz); - - JSONObject o = json.optJSONObject("userProperty" + i++); - if (o != null) { - if (p != null) { - p = p.reconfigure(req, o); - } else { - p = d.newInstance(req, o); - } - } - - if (p != null) { - p.setUser(this); - props.add(p); - } - } - this.properties = props; - - save(); - - if (oldFullName != null && !oldFullName.equals(this.fullName)) { - UserDetailsCache.get().invalidate(oldFullName); - } - - FormApply.success(".").generateResponse(req, rsp, this); - } - /** * Deletes this user from Hudson. */ diff --git a/core/src/main/java/hudson/model/UserProperty.java b/core/src/main/java/hudson/model/UserProperty.java index a9b9dbae7acd..a6ebeb738b23 100644 --- a/core/src/main/java/hudson/model/UserProperty.java +++ b/core/src/main/java/hudson/model/UserProperty.java @@ -24,9 +24,13 @@ package hudson.model; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.DescriptorExtensionList; import hudson.ExtensionPoint; import hudson.model.Descriptor.FormException; +import hudson.model.userproperty.UserPropertyCategory; +import java.util.ArrayList; +import java.util.List; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; @@ -58,6 +62,10 @@ public abstract class UserProperty implements ReconfigurableDescribable all( return Jenkins.get().getDescriptorList(UserProperty.class); } + /** + * Returns all the registered {@link UserPropertyCategory} descriptors for a given category. + * + * @since 2.468 + */ + public static List allByCategoryClass(@NonNull Class categoryClass) { + DescriptorExtensionList all = all(); + + List onlyForTheCategory = new ArrayList<>(all.size()); + for (UserPropertyDescriptor descriptor : all) { + if (descriptor.getUserPropertyCategory().getClass().equals(categoryClass)) { + onlyForTheCategory.add(descriptor); + } + } + + return onlyForTheCategory; + } + @Override public UserProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException { return form == null ? null : getDescriptor().newInstance(req, form); diff --git a/core/src/main/java/hudson/model/UserPropertyDescriptor.java b/core/src/main/java/hudson/model/UserPropertyDescriptor.java index 22e05ea59aff..66762bf3c716 100644 --- a/core/src/main/java/hudson/model/UserPropertyDescriptor.java +++ b/core/src/main/java/hudson/model/UserPropertyDescriptor.java @@ -24,6 +24,12 @@ package hudson.model; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.userproperty.UserPropertyCategory; +import java.util.Optional; +import org.jenkinsci.Symbol; + /** * {@link Descriptor} for {@link UserProperty}. * @@ -73,4 +79,51 @@ protected UserPropertyDescriptor() { public boolean isEnabled() { return true; } + + /** + * Define the category for this user property descriptor. + * + * @return never null, always the same value for a given instance of {@link Descriptor}. + * + * @since 2.468 + */ + public @NonNull UserPropertyCategory getUserPropertyCategory() { + // As this method is expected to be overloaded by subclasses + // the logic here is just done to support plugins with older core version + String categoryAsString = this.getUserPropertyCategoryAsString(); + if (categoryAsString != null) { + Optional firstIfFound = UserPropertyCategory.all().stream() + .filter(cat -> { + Symbol symbolAnnotation = cat.getClass().getAnnotation(Symbol.class); + if (symbolAnnotation != null) { + for (String symbolValue : symbolAnnotation.value()) { + if (symbolValue.equalsIgnoreCase(categoryAsString)) { + return true; + } + } + } + return false; + }) + .findFirst(); + if (firstIfFound.isPresent()) { + return firstIfFound.get(); + } + } + return UserPropertyCategory.get(UserPropertyCategory.Unclassified.class); + } + + /** + * Method proposed to prevent plugins to rely on too recent core version + * while keeping the possibility to use the categories. + * + * @deprecated This should only be used when the core requirement is below the version this method was added + * + * @return String name corresponding to the symbol of {@link #getUserPropertyCategory()} + * + * @since 2.468 + */ + @Deprecated + protected @CheckForNull String getUserPropertyCategoryAsString() { + return null; + } } diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index 0df916a19b5a..7b1bd98eb9ee 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -98,6 +98,8 @@ import net.sf.json.JSONObject; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; +import org.jenkins.ui.symbol.Symbol; +import org.jenkins.ui.symbol.SymbolRequest; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.DataBoundSetter; @@ -508,8 +510,7 @@ private boolean filterQueueItemTest(Queue.Item item, Collection vi } } // Check root project for sub-job projects (e.g. matrix jobs). - if (item.task instanceof AbstractProject) { - AbstractProject project = (AbstractProject) item.task; + if (item.task instanceof AbstractProject project) { return viewItems.contains(project.getRootProject()); } return false; @@ -801,11 +802,20 @@ public Categories doItemCategories(StaplerRequest req, StaplerResponse rsp, @Que String iconClassName = descriptor.getIconClassName(); if (iconClassName != null && !iconClassName.isBlank()) { metadata.put("iconClassName", iconClassName); - if (resUrl != null) { - Icon icon = IconSet.icons - .getIconByClassSpec(String.join(" ", iconClassName, iconStyle)); - if (icon != null) { - metadata.put("iconQualifiedUrl", icon.getQualifiedUrl(resUrl)); + if (iconClassName.startsWith("symbol-")) { + String iconXml = Symbol.get(new SymbolRequest.Builder() + .withName(iconClassName.split(" ")[0].substring(7)) + .withPluginName(Functions.extractPluginNameFromIconSrc(iconClassName)) + .withClasses("icon-xlg") + .build()); + metadata.put("iconXml", iconXml); + } else { + if (resUrl != null) { + Icon icon = IconSet.icons + .getIconByClassSpec(String.join(" ", iconClassName, iconStyle)); + if (icon != null) { + metadata.put("iconQualifiedUrl", icon.getQualifiedUrl(resUrl)); + } } } } @@ -844,8 +854,7 @@ public BuildTimelineWidget getTimeline() { public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { List lastBuilds = new ArrayList<>(); for (TopLevelItem item : getItems()) { - if (item instanceof Job) { - Job job = (Job) item; + if (item instanceof Job job) { Run lb = job.getLastBuild(); if (lb != null) lastBuilds.add(lb); } diff --git a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java index 00ef65c4a92e..8159f1f20cc1 100644 --- a/core/src/main/java/hudson/model/WorkspaceCleanupThread.java +++ b/core/src/main/java/hudson/model/WorkspaceCleanupThread.java @@ -24,6 +24,8 @@ package hudson.model; +import static hudson.Util.fileToPath; + import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; @@ -31,12 +33,19 @@ import hudson.FilePath; import hudson.Functions; import hudson.Util; +import hudson.remoting.VirtualChannel; +import hudson.slaves.WorkspaceList; +import java.io.File; +import java.io.FileFilter; import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.MasterToSlaveFileCallable; import jenkins.model.Jenkins; import jenkins.model.ModifiableTopLevelItemGroup; import jenkins.util.SystemProperties; @@ -90,8 +99,7 @@ public static void invoke() { if (check) { listener.getLogger().println("Deleting " + ws + " on " + node.getDisplayName()); try { - ws.deleteSuffixesRecursive(); - ws.deleteRecursive(); + ws.act(new CleanupOldWorkspaces(retainForDays)); } catch (IOException | InterruptedException x) { Functions.printStackTrace(x, listener.error("Failed to delete " + ws + " on " + node.getDisplayName())); } @@ -101,21 +109,6 @@ public static void invoke() { } private boolean shouldBeDeleted(@NonNull TopLevelItem item, FilePath dir, @NonNull Node n) throws IOException, InterruptedException { - // TODO: the use of remoting is not optimal. - // One remoting can execute "exists", "lastModified", and "delete" all at once. - // (Could even invert master loop so that one FileCallable takes care of all known items.) - if (!dir.exists()) { - LOGGER.log(Level.FINE, "Directory {0} does not exist", dir); - return false; - } - - // if younger than a month, keep it - long now = new Date().getTime(); - if (dir.lastModified() + retainForDays * DAY > now) { - LOGGER.log(Level.FINE, "Directory {0} is only {1} old, so not deleting", new Object[] {dir, Util.getTimeSpanString(now - dir.lastModified())}); - return false; - } - // TODO could also be good to add checkbox that lets users configure a workspace to never be auto-cleaned. // TODO check instead for SCMTriggerItem: @@ -143,11 +136,69 @@ private boolean shouldBeDeleted(@NonNull TopLevelItem item, FilePath dir, @NonNu return false; } } - - LOGGER.log(Level.FINER, "Going to delete directory {0}", dir); return true; } + private static class CleanupOldWorkspaces extends MasterToSlaveFileCallable { + + private final int retentionInDays; + + CleanupOldWorkspaces(int retentionInDays) { + this.retentionInDays = retentionInDays; + } + + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + File[] workspaces = null; + File parentWs = f.getParentFile(); + if (parentWs != null) { + workspaces = parentWs.listFiles(new ShouldBeDeletedFilter(this.retentionInDays, f.getName())); + } + + if (workspaces != null) { + for (File workspace : workspaces) { + LOGGER.log(Level.FINER, "Going to delete directory {0}", workspace); + Util.deleteRecursive(fileToPath(workspace), Path::toFile); + } + } + return null; + } + } + + private static class ShouldBeDeletedFilter implements FileFilter, Serializable { + + private final int retentionInDays; + + private final String workspaceBaseName; + + ShouldBeDeletedFilter(int retentionInDays, String workspaceBaseName) { + this.retentionInDays = retentionInDays; + this.workspaceBaseName = workspaceBaseName; + } + + @Override + public boolean accept(File dir) { + + if (!dir.isDirectory()) { + return false; + } + + // if not the workspace or a workspace suffix + if (!dir.getName().equals(workspaceBaseName) && !dir.getName().startsWith(workspaceBaseName + WorkspaceList.COMBINATOR)) { + return false; + } + + // if younger than a month, keep it + long now = new Date().getTime(); + if (dir.lastModified() + this.retentionInDays * DAY > now) { + LOGGER.log(Level.FINE, "Directory {0} is only {1} old, so not deleting", new Object[] {dir, Util.getTimeSpanString(now - dir.lastModified())}); + return false; + } + + return true; + } + } + private static final Logger LOGGER = Logger.getLogger(WorkspaceCleanupThread.class.getName()); /** diff --git a/core/src/main/java/hudson/model/listeners/ItemListener.java b/core/src/main/java/hudson/model/listeners/ItemListener.java index aef431c3207b..3f20e2704b68 100644 --- a/core/src/main/java/hudson/model/listeners/ItemListener.java +++ b/core/src/main/java/hudson/model/listeners/ItemListener.java @@ -35,6 +35,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import jenkins.util.Listeners; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Receives notifications about CRUD operations of {@link Item}. @@ -94,6 +96,16 @@ public void onCopied(Item src, Item item) { public void onLoaded() { } + /** + * Called before an item is deleted, providing the ability to veto the deletion operation before it starts. + * @param item the item being deleted + * @throws Failure to veto the operation. + * @throws InterruptedException If a blocking condition was interrupted, also vetoing the operation. + * @since 2.470 + */ + public void onCheckDelete(Item item) throws Failure, InterruptedException { + } + /** * Called right before a job is going to be deleted. * @@ -205,6 +217,19 @@ public static void fireOnUpdated(final Item item) { Listeners.notify(ItemListener.class, false, l -> l.onUpdated(item)); } + @Restricted(NoExternalUse.class) + public static void checkBeforeDelete(Item item) throws Failure, InterruptedException { + for (ItemListener l : all()) { + try { + l.onCheckDelete(item); + } catch (Failure e) { + throw e; + } catch (RuntimeException x) { + LOGGER.log(Level.WARNING, "failed to send event to listener of " + l.getClass(), x); + } + } + } + /** @since 1.548 */ public static void fireOnDeleted(final Item item) { Listeners.notify(ItemListener.class, false, l -> l.onDeleted(item)); diff --git a/core/src/main/java/hudson/model/queue/MappingWorksheet.java b/core/src/main/java/hudson/model/queue/MappingWorksheet.java index 54829ec0dac2..8150dd1bc132 100644 --- a/core/src/main/java/hudson/model/queue/MappingWorksheet.java +++ b/core/src/main/java/hudson/model/queue/MappingWorksheet.java @@ -47,6 +47,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Logger; /** * Defines a mapping problem for answering "where do we execute this task?" @@ -85,6 +86,9 @@ * @author Kohsuke Kawaguchi */ public class MappingWorksheet { + + private static final Logger LOGGER = Logger.getLogger(MappingWorksheet.class.getName()); + public final List executors; public final List works; /** @@ -135,8 +139,10 @@ public boolean canAccept(WorkChunk c) { if (c.assignedLabel != null && !c.assignedLabel.contains(node)) return false; // label mismatch - if (!(Node.SKIP_BUILD_CHECK_ON_FLYWEIGHTS && item.task instanceof Queue.FlyweightTask) && !nodeAcl.hasPermission2(item.authenticate2(), Computer.BUILD)) - return false; // tasks don't have a permission to run on this node + if (!(Node.SKIP_BUILD_CHECK_ON_FLYWEIGHTS && item.task instanceof Queue.FlyweightTask) && !nodeAcl.hasPermission2(item.authenticate2(), Computer.BUILD)) { + LOGGER.fine(() -> "Agent/Build permission denied to " + item.authenticate2().getName() + " on " + node.getNodeName()); + return false; + } return true; } @@ -184,7 +190,9 @@ public class WorkChunk extends ReadOnlyList { * If the previous execution of this task run on a certain node * and this task prefers to run on the same node, return that. * Otherwise null. + * @deprecated Unused. */ + @Deprecated public final ExecutorChunk lastBuiltOn; @@ -194,6 +202,7 @@ private WorkChunk(List base, int index) { this.index = index; this.assignedLabel = getAssignedLabel(base.get(0)); + @SuppressWarnings("deprecation") Node lbo = base.get(0).getLastBuiltOn(); for (ExecutorChunk ec : executors) { if (ec.node == lbo) { diff --git a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java index e2a113dfaa59..a36c5ca7c753 100644 --- a/core/src/main/java/hudson/model/queue/QueueTaskFilter.java +++ b/core/src/main/java/hudson/model/queue/QueueTaskFilter.java @@ -52,6 +52,7 @@ public Label getAssignedLabel() { return base.getAssignedLabel(); } + @Deprecated @Override public Node getLastBuiltOn() { return base.getLastBuiltOn(); diff --git a/core/src/main/java/hudson/model/queue/SubTask.java b/core/src/main/java/hudson/model/queue/SubTask.java index f8b7dd435088..0690d074617c 100644 --- a/core/src/main/java/hudson/model/queue/SubTask.java +++ b/core/src/main/java/hudson/model/queue/SubTask.java @@ -62,7 +62,9 @@ default Label getAssignedLabel() { * and this task prefers to run on the same node, return that. * Otherwise null. * @return by default, null + * @deprecated Unused. */ + @Deprecated default Node getLastBuiltOn() { return null; } diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java new file mode 100644 index 000000000000..5d5467b6eed4 --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategory.java @@ -0,0 +1,204 @@ +/* + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model.userproperty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.ModelObject; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; + +/** + * Grouping of related {@link UserProperty}s. + * + *

+ * To facilitate the separation of the user properties into multiple pages, tabs, and so on, + * {@link UserProperty}s are classified into categories (such as "security", "preferences", as well + * as the catch-all "unclassified".) Categories themselves are extensible — plugins may introduce + * its own category as well, although that should only happen if you are creating a big enough subsystem. + * + * @since 2.468 + * @see UserProperty + */ +public abstract class UserPropertyCategory implements ExtensionPoint, ModelObject { + /** + * One-line plain text message that explains what this category is about. + * This can be used in the UI to help the user pick the right category. + * + * The text should be longer than {@link #getDisplayName()} + */ + public abstract String getShortDescription(); + + /** + * Returns all the registered {@link UserPropertyCategory} descriptors. + */ + public static ExtensionList all() { + return ExtensionList.lookup(UserPropertyCategory.class); + } + + 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; + } + + /** + * This category is used when the {@link hudson.model.UserPropertyDescriptor} has not implemented + * the {@link UserPropertyDescriptor#getUserPropertyCategory()} method + * (or the getUserPropertyCategoryAsString method for compatibility reason). + *

+ * If you do not know what to use, choose the {@link Account} instead of this one. + */ + @Extension + @Symbol("unclassified") + @Restricted(DoNotUse.class) + public static class Unclassified extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Unclassified_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Unclassified_ShortDescription(); + } + } + + /** + * User property related to account settings (e.g. timezone, email, ...). + *

+ * It could be seen as the default choice for {@link UserProperty} that are defining their category. + * Currently it has the same effect as {@link Unclassified} but the behavior could change in the future. + */ + @Extension + @Symbol("account") + public static class Account extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Account_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Account_ShortDescription(); + } + } + + /** + * Preferences related configurations (e.g. notification type, default view, ...). + */ + @Extension + @Symbol("preferences") + public static class Preferences extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Preferences_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Preferences_ShortDescription(); + } + } + + /** + * Per user feature flags (e.g. new design, ...). + */ + @Extension + @Symbol("experimental") + public static class Experimental extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Experimental_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Experimental_ShortDescription(); + } + } + + /** + * User interface related configurations (e.g. theme, language, ...). + *

+ * See also {@link jenkins.appearance.AppearanceCategory}. + */ + @Extension + @Symbol("appearance") + public static class Appearance extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Appearance_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Appearance_ShortDescription(); + } + } + + + /** + * Security related configurations (e.g. API Token, SSH keys, ...). + * With this separation, we can more easily add control on their modifications. + */ + @Extension + @Symbol("security") + public static class Security extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Security_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Security_ShortDescription(); + } + } + + /** + * For user properties that are not expected to be displayed, + * typically automatically configured by automated behavior, without direct user interaction. + */ + @Extension + @Symbol("invisible") + public static class Invisible extends UserPropertyCategory { + @Override + public String getDisplayName() { + return Messages.UserPropertyCategory_Invisible_DisplayName(); + } + + @Override + public String getShortDescription() { + return Messages.UserPropertyCategory_Invisible_ShortDescription(); + } + } +} diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAccountAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAccountAction.java new file mode 100644 index 000000000000..822cdc6f4c99 --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAccountAction.java @@ -0,0 +1,121 @@ +/* + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model.userproperty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.DescriptorExtensionList; +import hudson.Extension; +import hudson.model.Action; +import hudson.model.Descriptor; +import hudson.model.TransientUserActionFactory; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.ServletException; +import jenkins.model.Jenkins; +import jenkins.security.UserDetailsCache; +import net.sf.json.JSONObject; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.verb.POST; + +@Restricted(NoExternalUse.class) +public class UserPropertyCategoryAccountAction extends UserPropertyCategoryAction implements Action { + public UserPropertyCategoryAccountAction(@NonNull User user) { + super(user); + } + + @Override + public String getDisplayName() { + return Messages.UserPropertyCategoryAccountAction_DisplayName(); + } + + @Override + public String getIconFileName() { + return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-settings" : null; + } + + @Override + public String getUrlName() { + return "account"; + } + + public @NonNull List getMyCategoryDescriptors() { + return allByTwoCategoryClasses(UserPropertyCategory.Unclassified.class, UserPropertyCategory.Account.class); + } + + private static List allByTwoCategoryClasses( + @NonNull Class categoryClass1, + @NonNull Class categoryClass2 + ) { + DescriptorExtensionList all = UserProperty.all(); + + List filteredList = new ArrayList<>(all.size()); + for (UserPropertyDescriptor descriptor : all) { + Class currClass = descriptor.getUserPropertyCategory().getClass(); + if (currClass.equals(categoryClass1) || currClass.equals(categoryClass2)) { + filteredList.add(descriptor); + } + } + + return filteredList; + } + + @POST + public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException { + User targetUser = this.getTargetUser(); + targetUser.checkPermission(Jenkins.ADMINISTER); + + JSONObject json = req.getSubmittedForm(); + + String oldFullName = targetUser.getFullName(); + targetUser.setFullName(json.getString("fullName")); + targetUser.setDescription(json.getString("description")); + + super.doConfigSubmit(req, rsp); + + if (!oldFullName.equals(targetUser.getFullName())) { + UserDetailsCache.get().invalidate(oldFullName); + } + } + + /** + * Inject the outer class configuration page into the sidenav and the request routing of the user + */ + @Extension(ordinal = 400) + @Symbol("account") + public static class AccountActionFactory extends TransientUserActionFactory { + public Collection createFor(User target) { + return Collections.singleton(new UserPropertyCategoryAccountAction(target)); + } + } +} diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAction.java new file mode 100644 index 000000000000..caec7c1bdf88 --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAction.java @@ -0,0 +1,65 @@ +package hudson.model.userproperty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Descriptor; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import hudson.util.FormApply; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.ServletException; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.verb.POST; + +public abstract class UserPropertyCategoryAction { + + private final User targetUser; + + public UserPropertyCategoryAction(User targetUser) { + this.targetUser = targetUser; + } + + public @NonNull User getTargetUser() { + return targetUser; + } + + public @NonNull abstract List getMyCategoryDescriptors(); + + @POST + public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException { + this.targetUser.checkPermission(Jenkins.ADMINISTER); + + JSONObject json = req.getSubmittedForm(); + + List props = new ArrayList<>(); + List myCategoryDescriptors = getMyCategoryDescriptors(); + int i = 0; + for (UserPropertyDescriptor d : myCategoryDescriptors) { + UserProperty p = this.targetUser.getProperty(d.clazz); + + JSONObject o = json.optJSONObject("userProperty" + i++); + if (o != null) { + if (p != null) { + p = p.reconfigure(req, o); + } else { + p = d.newInstance(req, o); + } + } + + if (p != null) { + props.add(p); + } + } + this.targetUser.addProperties(props); + + this.targetUser.save(); + + // we are in /user///, going to /user// + FormApply.success("..").generateResponse(req, rsp, this); + } +} diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAppearanceAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAppearanceAction.java new file mode 100644 index 000000000000..88d08a8add70 --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryAppearanceAction.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model.userproperty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Action; +import hudson.model.TransientUserActionFactory; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public class UserPropertyCategoryAppearanceAction extends UserPropertyCategoryAction implements Action { + public UserPropertyCategoryAppearanceAction(@NonNull User user) { + super(user); + } + + @Override + public String getDisplayName() { + return Messages.UserPropertyCategoryAppearanceAction_DisplayName(); + } + + @Override + public String getIconFileName() { + return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-brush-outline" : null; + } + + @Override + public String getUrlName() { + return "appearance"; + } + + public @NonNull List getMyCategoryDescriptors() { + return UserProperty.allByCategoryClass(UserPropertyCategory.Appearance.class); + } + + /** + * Inject the outer class configuration page into the sidenav and the request routing of the user + */ + @Extension(ordinal = 350) + @Symbol("appearance") + public static class AppearanceActionFactory extends TransientUserActionFactory { + public Collection createFor(User target) { + return Collections.singleton(new UserPropertyCategoryAppearanceAction(target)); + } + } +} diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryExperimentalAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryExperimentalAction.java new file mode 100644 index 000000000000..bb242bacad7a --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryExperimentalAction.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model.userproperty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Action; +import hudson.model.TransientUserActionFactory; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public class UserPropertyCategoryExperimentalAction extends UserPropertyCategoryAction implements Action { + public UserPropertyCategoryExperimentalAction(@NonNull User user) { + super(user); + } + + @Override + public String getDisplayName() { + return Messages.UserPropertyCategoryExperimentalAction_DisplayName(); + } + + @Override + public String getIconFileName() { + return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-flask" : null; + } + + @Override + public String getUrlName() { + return "experiments"; + } + + public @NonNull List getMyCategoryDescriptors() { + return UserProperty.allByCategoryClass(UserPropertyCategory.Experimental.class); + } + + /** + * Inject the outer class configuration page into the sidenav and the request routing of the user + */ + @Extension(ordinal = 100) + @Symbol("experimental") + public static class ExperimentalActionFactory extends TransientUserActionFactory { + public Collection createFor(User target) { + return Collections.singleton(new UserPropertyCategoryExperimentalAction(target)); + } + } +} diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryPreferencesAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryPreferencesAction.java new file mode 100644 index 000000000000..7a74b702e6c0 --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategoryPreferencesAction.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model.userproperty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Action; +import hudson.model.TransientUserActionFactory; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public class UserPropertyCategoryPreferencesAction extends UserPropertyCategoryAction implements Action { + public UserPropertyCategoryPreferencesAction(@NonNull User user) { + super(user); + } + + @Override + public String getDisplayName() { + return Messages.UserPropertyCategoryPreferencesAction_DisplayName(); + } + + @Override + public String getIconFileName() { + return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-parameters" : null; + } + + @Override + public String getUrlName() { + return "preferences"; + } + + public @NonNull List getMyCategoryDescriptors() { + return UserProperty.allByCategoryClass(UserPropertyCategory.Preferences.class); + } + + /** + * Inject the outer class configuration page into the sidenav and the request routing of the user + */ + @Extension(ordinal = 300) + @Symbol("preferences") + public static class PreferencesActionFactory extends TransientUserActionFactory { + public Collection createFor(User target) { + return Collections.singleton(new UserPropertyCategoryPreferencesAction(target)); + } + } +} diff --git a/core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java b/core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java new file mode 100644 index 000000000000..a6cb3e6ed3c4 --- /dev/null +++ b/core/src/main/java/hudson/model/userproperty/UserPropertyCategorySecurityAction.java @@ -0,0 +1,77 @@ +/* + * The MIT License + * + * Copyright (c) 2022, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model.userproperty; + + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Action; +import hudson.model.TransientUserActionFactory; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.model.UserPropertyDescriptor; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import jenkins.model.Jenkins; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public class UserPropertyCategorySecurityAction extends UserPropertyCategoryAction implements Action { + public UserPropertyCategorySecurityAction(@NonNull User user) { + super(user); + } + + @Override + public String getDisplayName() { + return Messages.UserPropertyCategorySecurityAction_DisplayName(); + } + + @Override + public String getIconFileName() { + return getTargetUser().hasPermission(Jenkins.ADMINISTER) ? "symbol-lock-closed" : null; + } + + @Override + public String getUrlName() { + return "security"; + } + + public @NonNull List getMyCategoryDescriptors() { + return UserProperty.allByCategoryClass(UserPropertyCategory.Security.class); + } + + /** + * Inject the outer class configuration page into the sidenav and the request routing of the user + */ + @Extension(ordinal = 200) + @Symbol("security") + public static class SecurityActionFactory extends TransientUserActionFactory { + public Collection createFor(User target) { + return Collections.singleton(new UserPropertyCategorySecurityAction(target)); + } + } +} diff --git a/core/src/main/java/hudson/scm/ChangeLogSet.java b/core/src/main/java/hudson/scm/ChangeLogSet.java index a0c4a4c9cd79..f9f29720355f 100644 --- a/core/src/main/java/hudson/scm/ChangeLogSet.java +++ b/core/src/main/java/hudson/scm/ChangeLogSet.java @@ -238,7 +238,7 @@ public Collection getAffectedFiles() { ChangeLogSet parent = getParent(); if (null != parent) { String kind = parent.getKind(); - if (null != kind && kind.trim().length() > 0) scm = kind; + if (null != kind && !kind.trim().isEmpty()) scm = kind; } throw new UnsupportedOperationException("getAffectedFiles() is not implemented by " + scm); } diff --git a/core/src/main/java/hudson/scm/browsers/QueryBuilder.java b/core/src/main/java/hudson/scm/browsers/QueryBuilder.java index 06d933aa7ef3..72e6ebc0d557 100644 --- a/core/src/main/java/hudson/scm/browsers/QueryBuilder.java +++ b/core/src/main/java/hudson/scm/browsers/QueryBuilder.java @@ -38,7 +38,7 @@ public QueryBuilder(String s) { public QueryBuilder add(String s) { if (s == null) return this; // nothing to add - if (buf.length() == 0) buf.append('?'); + if (buf.isEmpty()) buf.append('?'); else buf.append('&'); buf.append(s); return this; diff --git a/core/src/main/java/hudson/search/FixedSet.java b/core/src/main/java/hudson/search/FixedSet.java index 3c3819f03bcb..f77e20623542 100644 --- a/core/src/main/java/hudson/search/FixedSet.java +++ b/core/src/main/java/hudson/search/FixedSet.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Locale; /** * Set of {@link SearchItem}s that are statically known upfront. @@ -61,7 +62,7 @@ public void suggest(String token, List result) { boolean caseInsensitive = UserSearchProperty.isCaseInsensitive(); for (SearchItem i : items) { String name = i.getSearchName(); - if (name != null && (name.contains(token) || (caseInsensitive && name.toLowerCase().contains(token.toLowerCase())))) { + if (name != null && (name.contains(token) || (caseInsensitive && name.toLowerCase(Locale.ROOT).contains(token.toLowerCase(Locale.ROOT))))) { result.add(i); } } diff --git a/core/src/main/java/hudson/search/ParsedQuickSilver.java b/core/src/main/java/hudson/search/ParsedQuickSilver.java index dc214892ac00..879f9c080d16 100644 --- a/core/src/main/java/hudson/search/ParsedQuickSilver.java +++ b/core/src/main/java/hudson/search/ParsedQuickSilver.java @@ -85,7 +85,7 @@ private ParsedQuickSilver(Class clazz) { private String splitName(String url) { StringBuilder buf = new StringBuilder(url.length() + 5); for (String token : url.split("(?<=[a-z])(?=[A-Z])")) { - if (buf.length() > 0) buf.append(' '); + if (!buf.isEmpty()) buf.append(' '); buf.append(Introspector.decapitalize(token)); } return buf.toString(); diff --git a/core/src/main/java/hudson/search/SuggestedItem.java b/core/src/main/java/hudson/search/SuggestedItem.java index 86dbc36b5fe3..9ba455270e58 100644 --- a/core/src/main/java/hudson/search/SuggestedItem.java +++ b/core/src/main/java/hudson/search/SuggestedItem.java @@ -111,7 +111,7 @@ private void getUrl(StringBuilder buf) { buf.setLength(0); buf.append(f); } else { - if (buf.length() == 0 || buf.charAt(buf.length() - 1) != '/') + if (buf.isEmpty() || buf.charAt(buf.length() - 1) != '/') buf.append('/'); buf.append(f); } diff --git a/core/src/main/java/hudson/search/UserSearchProperty.java b/core/src/main/java/hudson/search/UserSearchProperty.java index 7e9836944c0b..a3515dd08874 100644 --- a/core/src/main/java/hudson/search/UserSearchProperty.java +++ b/core/src/main/java/hudson/search/UserSearchProperty.java @@ -5,6 +5,7 @@ import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; +import hudson.model.userproperty.UserPropertyCategory; import net.sf.json.JSONObject; import org.jenkinsci.Symbol; import org.kohsuke.stapler.StaplerRequest; @@ -54,6 +55,10 @@ public UserProperty newInstance(StaplerRequest req, JSONObject formData) throws return new UserSearchProperty(formData.optBoolean("insensitiveSearch")); } + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Preferences.class); + } } } diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java index 6305a69a6abb..bd122244c7e2 100644 --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java @@ -39,6 +39,7 @@ import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; +import hudson.model.userproperty.UserPropertyCategory; import hudson.security.FederatedLoginService.FederatedIdentity; import hudson.security.captcha.CaptchaSupport; import hudson.util.FormValidation; @@ -449,7 +450,7 @@ private SignupInfo validateAccountCreationForm(StaplerRequest req, boolean valid si.errors.put("password1", Messages.HudsonPrivateSecurityRealm_CreateAccount_PasswordNotMatch()); } - if (!(si.password1 != null && si.password1.length() != 0)) { + if (!(si.password1 != null && !si.password1.isEmpty())) { si.errors.put("password1", Messages.HudsonPrivateSecurityRealm_CreateAccount_PasswordRequired()); } @@ -801,7 +802,6 @@ public boolean equals(Object o) { public int hashCode() { return getUsername().hashCode(); } - } public static class ConverterImpl extends XStream2.PassthruConverter

{ @@ -884,6 +884,11 @@ public boolean isEnabled() { public UserProperty newInstance(User user) { return null; } + + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Security.class); + } } } diff --git a/core/src/main/java/hudson/security/SecurityRealm.java b/core/src/main/java/hudson/security/SecurityRealm.java index 525368f65692..b969a57eb781 100644 --- a/core/src/main/java/hudson/security/SecurityRealm.java +++ b/core/src/main/java/hudson/security/SecurityRealm.java @@ -326,7 +326,7 @@ public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException Authentication auth = SecurityContextHolder.getContext().getAuthentication(); SecurityContextHolder.clearContext(); - String contextPath = req.getContextPath().length() > 0 ? req.getContextPath() : "/"; + String contextPath = !req.getContextPath().isEmpty() ? req.getContextPath() : "/"; resetRememberMeCookie(req, rsp, contextPath); clearStaleSessionCookies(req, rsp, contextPath); diff --git a/core/src/main/java/hudson/security/csrf/CrumbFilter.java b/core/src/main/java/hudson/security/csrf/CrumbFilter.java index 03e17a0fd894..8b4b558938e7 100644 --- a/core/src/main/java/hudson/security/csrf/CrumbFilter.java +++ b/core/src/main/java/hudson/security/csrf/CrumbFilter.java @@ -105,7 +105,7 @@ private static String canonicalPath(String path) { buf.append(token); } // translation: if (path.endsWith("/") && !buf.endsWith("/")) - if (path.endsWith("/") && (buf.length() == 0 || buf.charAt(buf.length() - 1) != '/')) + if (path.endsWith("/") && (buf.isEmpty() || buf.charAt(buf.length() - 1) != '/')) buf.append('/'); return buf.toString(); } diff --git a/core/src/main/java/hudson/security/csrf/CrumbIssuer.java b/core/src/main/java/hudson/security/csrf/CrumbIssuer.java index 384f0a765ddd..cd0c51b03fb0 100644 --- a/core/src/main/java/hudson/security/csrf/CrumbIssuer.java +++ b/core/src/main/java/hudson/security/csrf/CrumbIssuer.java @@ -78,7 +78,7 @@ public String getCrumb(ServletRequest request) { if (crumb == null) { crumb = issueCrumb(request, getDescriptor().getCrumbSalt()); if (request != null) { - if (crumb != null && crumb.length() > 0) { + if (crumb != null && !crumb.isEmpty()) { request.setAttribute(CRUMB_ATTRIBUTE, crumb); } else { request.removeAttribute(CRUMB_ATTRIBUTE); @@ -204,7 +204,7 @@ public static class RestrictedApi extends Api { text = null; } if (text != null) { - try (OutputStream o = rsp.getCompressedOutputStream(req)) { + try (OutputStream o = rsp.getOutputStream()) { rsp.setContentType("text/plain;charset=UTF-8"); o.write(text.getBytes(StandardCharsets.UTF_8)); } diff --git a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java index ec0142dd4768..e4601a2fad31 100644 --- a/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java +++ b/core/src/main/java/hudson/slaves/CloudRetentionStrategy.java @@ -64,7 +64,7 @@ public long check(final AbstractCloudComputer c) { } } } - return 1; + return 0; } /** diff --git a/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java b/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java index 4e1b376caffe..a8dfeba1b20a 100644 --- a/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java +++ b/core/src/main/java/hudson/slaves/CloudSlaveRetentionStrategy.java @@ -37,7 +37,7 @@ public long check(T c) { } } } - return checkCycle(); + return TimeUnit.MILLISECONDS.toMinutes(checkCycle()); } /** diff --git a/core/src/main/java/hudson/slaves/ComputerRetentionWork.java b/core/src/main/java/hudson/slaves/ComputerRetentionWork.java index ddee61115a03..42256855117d 100644 --- a/core/src/main/java/hudson/slaves/ComputerRetentionWork.java +++ b/core/src/main/java/hudson/slaves/ComputerRetentionWork.java @@ -25,13 +25,15 @@ package hudson.slaves; import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.AperiodicWork; import hudson.model.Computer; import hudson.model.Node; -import hudson.model.PeriodicWork; import hudson.model.Queue; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; +import jenkins.model.GlobalComputerRetentionCheckIntervalConfiguration; import jenkins.model.Jenkins; import org.jenkinsci.Symbol; @@ -42,7 +44,7 @@ * @author Stephen Connolly */ @Extension @Symbol("computerRetention") -public class ComputerRetentionWork extends PeriodicWork { +public class ComputerRetentionWork extends AperiodicWork { /** * Use weak hash map to avoid leaking {@link Computer}. @@ -51,12 +53,18 @@ public class ComputerRetentionWork extends PeriodicWork { @Override public long getRecurrencePeriod() { - return MIN; + return ExtensionList.lookupSingleton(GlobalComputerRetentionCheckIntervalConfiguration.class).getComputerRetentionCheckInterval() * 1000L; + } + + @Override + public AperiodicWork getNewInstance() { + // ComputerRetentionWork is a singleton. + return this; } @SuppressWarnings("unchecked") @Override - protected void doRun() { + protected void doAperiodicRun() { final long startRun = System.currentTimeMillis(); for (final Computer c : Jenkins.get().getComputers()) { Queue.withLock(new Runnable() { @@ -67,8 +75,7 @@ public void run() { return; if (!nextCheck.containsKey(c) || startRun > nextCheck.get(c)) { // at the moment I don't trust strategies to wait more than 60 minutes - // strategies need to wait at least one minute - final long waitInMins = Math.max(1, Math.min(60, c.getRetentionStrategy().check(c))); + final long waitInMins = Math.max(0, Math.min(60, c.getRetentionStrategy().check(c))); nextCheck.put(c, startRun + TimeUnit.MINUTES.toMillis(waitInMins)); } } diff --git a/core/src/main/java/hudson/slaves/JNLPLauncher.java b/core/src/main/java/hudson/slaves/JNLPLauncher.java index 9add9faf0c3f..4f0cbc1e6153 100644 --- a/core/src/main/java/hudson/slaves/JNLPLauncher.java +++ b/core/src/main/java/hudson/slaves/JNLPLauncher.java @@ -175,7 +175,7 @@ public String getTunnel() { */ @DataBoundSetter public void setTunnel(String tunnel) { - this.tunnel = tunnel; + this.tunnel = Util.fixEmptyAndTrim(tunnel); } @Override diff --git a/core/src/main/java/hudson/slaves/RetentionStrategy.java b/core/src/main/java/hudson/slaves/RetentionStrategy.java index e06b8e42c069..687e44d5c172 100644 --- a/core/src/main/java/hudson/slaves/RetentionStrategy.java +++ b/core/src/main/java/hudson/slaves/RetentionStrategy.java @@ -171,7 +171,7 @@ public Always() { public long check(SlaveComputer c) { if (c.isOffline() && !c.isConnecting() && c.isLaunchSupported()) c.tryReconnect(); - return 1; + return 0; } @Extension(ordinal = 100) @Symbol("always") @@ -285,7 +285,7 @@ public long check(final SlaveComputer c) { return TimeUnit.MILLISECONDS.toMinutes(TimeUnit.MINUTES.toMillis(idleDelay) - idleMilliseconds); } } - return 1; + return 0; } @Extension @Symbol("demand") diff --git a/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java b/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java index ef6e3d0c8658..ea90529b29b4 100644 --- a/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java +++ b/core/src/main/java/hudson/slaves/SimpleScheduledRetentionStrategy.java @@ -209,7 +209,7 @@ public void run() { LOGGER.log(INFO, "Disabling new jobs for computer {0} as it has finished its scheduled uptime", new Object[]{c.getName()}); - return 1; + return 0; } else if (c.isIdle() && c.isAcceptingTasks()) { Queue.withLock(new Runnable() { @Override @@ -243,7 +243,7 @@ public void run() { } } } - return 1; + return 0; } private synchronized boolean isOnlineScheduled() { diff --git a/core/src/main/java/hudson/tasks/ArtifactArchiver.java b/core/src/main/java/hudson/tasks/ArtifactArchiver.java index 02137ad36946..4300cd1538ab 100644 --- a/core/src/main/java/hudson/tasks/ArtifactArchiver.java +++ b/core/src/main/java/hudson/tasks/ArtifactArchiver.java @@ -389,7 +389,9 @@ public boolean isApplicable(Class jobType) { if (bd instanceof LogRotator) { LogRotator lr = (LogRotator) bd; if (lr.getArtifactNumToKeep() == -1) { - p.setBuildDiscarder(new LogRotator(lr.getDaysToKeep(), lr.getNumToKeep(), lr.getArtifactDaysToKeep(), 1)); + LogRotator newLr = new LogRotator(lr.getDaysToKeep(), lr.getNumToKeep(), lr.getArtifactDaysToKeep(), 1); + newLr.setRemoveLastBuild(lr.isRemoveLastBuild()); + p.setBuildDiscarder(newLr); } else { LOG.log(Level.WARNING, "will not clobber artifactNumToKeep={0} in {1}", new Object[] {lr.getArtifactNumToKeep(), p}); } diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java index 3be7631bb1f0..4d843b27b003 100644 --- a/core/src/main/java/hudson/tasks/Fingerprinter.java +++ b/core/src/main/java/hudson/tasks/Fingerprinter.java @@ -182,7 +182,7 @@ public void perform(Run build, FilePath workspace, EnvVars environment, La Map record = new HashMap<>(); - if (targets.length() != 0) { + if (!targets.isEmpty()) { String expandedTargets = targets; if (build instanceof AbstractBuild) { // no expansion for pipelines expandedTargets = environment.expand(expandedTargets); diff --git a/core/src/main/java/hudson/tasks/LogRotator.java b/core/src/main/java/hudson/tasks/LogRotator.java index 3d5f3975808a..c22d5af2e3d2 100644 --- a/core/src/main/java/hudson/tasks/LogRotator.java +++ b/core/src/main/java/hudson/tasks/LogRotator.java @@ -50,6 +50,7 @@ import jenkins.util.io.CompositeIOException; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; /** * Default implementation of {@link BuildDiscarder}. @@ -108,6 +109,12 @@ public CollatedLogRotatorException(String msg, Collection values) { */ private final Integer artifactNumToKeep; + /** + * If enabled also remove last successful build. + * @since TODO + */ + private boolean removeLastBuild; + @DataBoundConstructor public LogRotator(String daysToKeepStr, String numToKeepStr, String artifactDaysToKeepStr, String artifactNumToKeepStr) { this (parse(daysToKeepStr), parse(numToKeepStr), @@ -140,6 +147,11 @@ public LogRotator(int daysToKeep, int numToKeep, int artifactDaysToKeep, int art } + @DataBoundSetter + public void setRemoveLastBuild(boolean removeLastBuild) { + this.removeLastBuild = removeLastBuild; + } + @Override @SuppressWarnings("rawtypes") public void perform(Job job) throws IOException, InterruptedException { @@ -148,9 +160,9 @@ public void perform(Job job) throws IOException, InterruptedException { LOGGER.log(FINE, "Running the log rotation for {0} with numToKeep={1} daysToKeep={2} artifactNumToKeep={3} artifactDaysToKeep={4}", new Object[] {job, numToKeep, daysToKeep, artifactNumToKeep, artifactDaysToKeep}); - // always keep the last successful and the last stable builds - Run lsb = job.getLastSuccessfulBuild(); - Run lstb = job.getLastStableBuild(); + // if configured keep the last successful and the last stable builds + Run lsb = removeLastBuild ? null : job.getLastSuccessfulBuild(); + Run lstb = removeLastBuild ? null : job.getLastStableBuild(); if (numToKeep != -1) { // Note that RunList.size is deprecated, and indeed here we are loading all the builds of the job. @@ -270,6 +282,10 @@ public int getArtifactNumToKeep() { return unbox(artifactNumToKeep); } + public boolean isRemoveLastBuild() { + return removeLastBuild; + } + public String getDaysToKeepStr() { return toString(daysToKeep); } diff --git a/core/src/main/java/hudson/tools/AbstractCommandInstaller.java b/core/src/main/java/hudson/tools/AbstractCommandInstaller.java index 46e7bc6657a2..88cbe5915cc0 100644 --- a/core/src/main/java/hudson/tools/AbstractCommandInstaller.java +++ b/core/src/main/java/hudson/tools/AbstractCommandInstaller.java @@ -90,7 +90,7 @@ public abstract static class Descriptor { public FormValidation doCheckCommand(@QueryParameter String value) { - if (value.length() > 0) { + if (!value.isEmpty()) { return FormValidation.ok(); } else { return FormValidation.error(Messages.CommandInstaller_no_command()); @@ -98,7 +98,7 @@ public FormValidation doCheckCommand(@QueryParameter String value) { } public FormValidation doCheckToolHome(@QueryParameter String value) { - if (value.length() > 0) { + if (!value.isEmpty()) { return FormValidation.ok(); } else { return FormValidation.error(Messages.CommandInstaller_no_toolHome()); diff --git a/core/src/main/java/hudson/tools/InstallerTranslator.java b/core/src/main/java/hudson/tools/InstallerTranslator.java index 35db845dd2e6..45e24cadf60d 100644 --- a/core/src/main/java/hudson/tools/InstallerTranslator.java +++ b/core/src/main/java/hudson/tools/InstallerTranslator.java @@ -59,11 +59,8 @@ public String getToolHome(Node node, ToolInstallation tool, TaskListener log) th if (installer.appliesTo(node)) { Semaphore semaphore; synchronized (mutexByNode) { - Map mutexByTool = mutexByNode.computeIfAbsent(node, k -> new WeakHashMap<>()); - semaphore = mutexByTool.get(tool); - if (semaphore == null) { - mutexByTool.put(tool, semaphore = new Semaphore(1)); - } + Map mutexByTool = mutexByNode.computeIfAbsent(node, unused -> new WeakHashMap<>()); + semaphore = mutexByTool.computeIfAbsent(tool, unused -> new Semaphore(1)); } semaphore.acquire(); try { diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java index e35a3b66af69..43fdc6bb1062 100644 --- a/core/src/main/java/hudson/triggers/SCMTrigger.java +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java @@ -468,7 +468,7 @@ public String getUrlName() { */ public void doPollingLog(StaplerRequest req, StaplerResponse rsp) throws IOException { rsp.setContentType("text/plain;charset=UTF-8"); - try (OutputStream os = rsp.getCompressedOutputStream(req); + try (OutputStream os = rsp.getOutputStream(); // Prevent jelly from flushing stream so Content-Length header can be added afterwards FlushProofOutputStream out = new FlushProofOutputStream(os)) { getPollingLogText().writeLogTo(0, out); diff --git a/core/src/main/java/hudson/util/ArgumentListBuilder.java b/core/src/main/java/hudson/util/ArgumentListBuilder.java index 245f3e74a99b..9b2c7bebb594 100644 --- a/core/src/main/java/hudson/util/ArgumentListBuilder.java +++ b/core/src/main/java/hudson/util/ArgumentListBuilder.java @@ -295,7 +295,7 @@ public List toList() { public String toStringWithQuote() { StringBuilder buf = new StringBuilder(); for (String arg : args) { - if (buf.length() > 0) buf.append(' '); + if (!buf.isEmpty()) buf.append(' '); if (arg.indexOf(' ') >= 0 || arg.isEmpty()) buf.append('"').append(arg).append('"'); @@ -400,7 +400,7 @@ private static boolean startQuoting(StringBuilder buf, String arg, int atIndex) * @return true if there are any masked arguments; false otherwise */ public boolean hasMaskedArguments() { - return mask.length() > 0; + return !mask.isEmpty(); } /** @@ -437,7 +437,7 @@ public String toString() { if (mask.get(i)) arg = "******"; - if (buf.length() > 0) buf.append(' '); + if (!buf.isEmpty()) buf.append(' '); if (arg.indexOf(' ') >= 0 || arg.isEmpty()) buf.append('"').append(arg).append('"'); diff --git a/core/src/main/java/hudson/util/BootFailure.java b/core/src/main/java/hudson/util/BootFailure.java index a460ffea148e..f23cf7fa66b7 100644 --- a/core/src/main/java/hudson/util/BootFailure.java +++ b/core/src/main/java/hudson/util/BootFailure.java @@ -15,6 +15,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletContext; +import jenkins.model.Jenkins; import jenkins.util.groovy.GroovyHookScript; import org.kohsuke.stapler.WebApp; @@ -51,6 +52,7 @@ public void publish(ServletContext context, @CheckForNull File home) { .bind("servletContext", context) .bind("attempts", loadAttempts(home)) .run(); + Jenkins.get().getLifecycle().onBootFailure(this); } /** diff --git a/core/src/main/java/hudson/util/CompressedFile.java b/core/src/main/java/hudson/util/CompressedFile.java index d7f42ca7a9ae..cd08f838a250 100644 --- a/core/src/main/java/hudson/util/CompressedFile.java +++ b/core/src/main/java/hudson/util/CompressedFile.java @@ -24,8 +24,6 @@ package hudson.util; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; import hudson.Util; import java.io.File; import java.io.FileNotFoundException; @@ -43,6 +41,8 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; /** * Represents write-once read-many file that can be optionally compressed diff --git a/core/src/main/java/hudson/util/DescribableList.java b/core/src/main/java/hudson/util/DescribableList.java index a73dac196b2c..9c62af2f191a 100644 --- a/core/src/main/java/hudson/util/DescribableList.java +++ b/core/src/main/java/hudson/util/DescribableList.java @@ -219,8 +219,7 @@ public void rebuildHetero(StaplerRequest req, JSONObject formData, Collection masks) { - super(parent); + super("Masking ClassLoader of " + parent.getName(), parent); this.masksClasses = List.copyOf(masks); /* diff --git a/core/src/main/java/hudson/util/MultipartFormDataParser.java b/core/src/main/java/hudson/util/MultipartFormDataParser.java index 8633a77e1534..f173cff9802b 100644 --- a/core/src/main/java/hudson/util/MultipartFormDataParser.java +++ b/core/src/main/java/hudson/util/MultipartFormDataParser.java @@ -27,17 +27,21 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.fileupload.FileCountLimitExceededException; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadBase; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload2.core.DiskFileItem; +import org.apache.commons.fileupload2.core.DiskFileItemFactory; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadSizeException; +import org.apache.commons.fileupload2.javax.JavaxServletDiskFileUpload; +import org.apache.commons.fileupload2.javax.JavaxServletFileUpload; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -52,7 +56,7 @@ public class MultipartFormDataParser implements AutoCloseable { /** * Limits the number of form fields that can be processed in one multipart/form-data request. - * Used to set {@link org.apache.commons.fileupload.servlet.ServletFileUpload#setFileCountMax(long)}. + * Used to set {@link org.apache.commons.fileupload2.javax.JavaxServletFileUpload#setFileCountMax(long)}. * Despite the name, this applies to all form fields, not just actual file attachments. * Set to {@code -1} to disable limits. */ @@ -60,7 +64,7 @@ public class MultipartFormDataParser implements AutoCloseable { /** * Limits the size (in bytes) of individual fields that can be processed in one multipart/form-data request. - * Used to set {@link org.apache.commons.fileupload.servlet.ServletFileUpload#setFileSizeMax(long)}. + * Used to set {@link org.apache.commons.fileupload2.javax.JavaxServletFileUpload#setFileSizeMax(long)}. * Despite the name, this applies to all form fields, not just actual file attachments. * Set to {@code -1} to disable limits. */ @@ -68,7 +72,7 @@ public class MultipartFormDataParser implements AutoCloseable { /** * Limits the total request size (in bytes) that can be processed in one multipart/form-data request. - * Used to set {@link org.apache.commons.fileupload.servlet.ServletFileUpload#setSizeMax(long)}. + * Used to set {@link org.apache.commons.fileupload2.javax.JavaxServletFileUpload#setSizeMax(long)}. * Set to {@code -1} to disable limits. */ private static /* nonfinal for Jenkins script console */ long FILEUPLOAD_MAX_SIZE = Long.getLong(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_SIZE", -1); @@ -82,20 +86,20 @@ public MultipartFormDataParser(HttpServletRequest request, int maxParts, long ma throw new ServletException("Error creating temporary directory", e); } tmpDir.deleteOnExit(); - ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, tmpDir)); + JavaxServletFileUpload upload = new JavaxServletDiskFileUpload(DiskFileItemFactory.builder().setFile(tmpDir).get()); upload.setFileCountMax(maxParts); upload.setFileSizeMax(maxPartSize); upload.setSizeMax(maxSize); try { for (FileItem fi : upload.parseRequest(request)) byName.put(fi.getFieldName(), fi); - } catch (FileCountLimitExceededException e) { + } catch (FileUploadFileCountLimitException e) { throw new ServletException("File upload field count limit exceeded. Consider setting the Java system property " + MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES to a value greater than " + FILEUPLOAD_MAX_FILES + ", or to -1 to disable this limit.", e); - } catch (FileUploadBase.FileSizeLimitExceededException e) { + } catch (FileUploadByteCountLimitException e) { throw new ServletException("File upload field size limit exceeded. Consider setting the Java system property " + MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILE_SIZE to a value greater than " + FILEUPLOAD_MAX_FILE_SIZE + ", or to -1 to disable this limit.", e); - } catch (FileUploadBase.SizeLimitExceededException e) { + } catch (FileUploadSizeException e) { throw new ServletException("File upload total size limit exceeded. Consider setting the Java system property " + MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_SIZE to a value greater than " + FILEUPLOAD_MAX_SIZE + ", or to -1 to disable this limit.", e); } catch (FileUploadException e) { @@ -118,17 +122,30 @@ public String get(String key) { return fi.getString(); } - public FileItem getFileItem(String key) { + public FileItem getFileItem2(String key) { return byName.get(key); } + /** + * @deprecated use {@link #getFileItem2(String)} + */ + @Deprecated + public org.apache.commons.fileupload.FileItem getFileItem(String key) { + return org.apache.commons.fileupload.FileItem.fromFileUpload2FileItem(getFileItem2(key)); + } + /** * If any file is created on the disk, delete them all. * Even if this method is not called, the resource will be still cleaned up later by GC. */ public void cleanUp() { - for (FileItem item : byName.values()) - item.delete(); + for (FileItem item : byName.values()) { + try { + item.delete(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } /** Alias for {@link #cleanUp}. */ diff --git a/core/src/main/java/hudson/util/ProcessTree.java b/core/src/main/java/hudson/util/ProcessTree.java index 8fbb80c8a8db..21e811a877fd 100644 --- a/core/src/main/java/hudson/util/ProcessTree.java +++ b/core/src/main/java/hudson/util/ProcessTree.java @@ -455,7 +455,7 @@ public static ProcessTree get() { } // Null-check in case the previous call worked - boolean vetoes = vetoersExist == null ? true : vetoersExist; + boolean vetoes = vetoersExist == null || vetoersExist; try { if (File.pathSeparatorChar == ';') @@ -2115,12 +2115,12 @@ public T act(ProcessCallable callable) throws IOException, InterruptedExc } /* - On MacOS X, there's no procfs - instead you'd do it with the sysctl - + On MacOS X, there's no procfs + instead you'd do it with sysctl + There's CLI but that doesn't seem to offer the access to per-process info - + diff --git a/core/src/main/java/hudson/util/RemotingDiagnostics.java b/core/src/main/java/hudson/util/RemotingDiagnostics.java index 1756d2933b18..06a42beee69d 100644 --- a/core/src/main/java/hudson/util/RemotingDiagnostics.java +++ b/core/src/main/java/hudson/util/RemotingDiagnostics.java @@ -217,7 +217,7 @@ public void doHeapDump(StaplerRequest req, StaplerResponse rsp) throws IOExcepti FilePath dump = obtain(); try { - dump.copyTo(rsp.getCompressedOutputStream(req)); + dump.copyTo(rsp.getOutputStream()); } finally { dump.delete(); } diff --git a/core/src/main/java/hudson/util/SecretRewriter.java b/core/src/main/java/hudson/util/SecretRewriter.java deleted file mode 100644 index 4e5015852353..000000000000 --- a/core/src/main/java/hudson/util/SecretRewriter.java +++ /dev/null @@ -1,213 +0,0 @@ -package hudson.util; - -import hudson.Functions; -import hudson.Util; -import hudson.model.TaskListener; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.util.Base64; -import java.util.HashSet; -import java.util.Set; -import javax.crypto.Cipher; -import javax.crypto.SecretKey; - -/** - * Rewrites XML files by looking for Secrets that are stored with the old key and replaces them - * by the new encrypted values. - * - * @author Kohsuke Kawaguchi - */ -public class SecretRewriter { - private final Cipher cipher; - private final SecretKey key; - - /** - * How many files have been scanned? - */ - private int count; - - /** - * Canonical paths of the directories we are recursing to protect - * against symlink induced cycles. - */ - private Set callstack = new HashSet<>(); - - public SecretRewriter() throws GeneralSecurityException { - cipher = Secret.getCipher("AES"); - key = HistoricalSecrets.getLegacyKey(); - } - - /** @deprecated SECURITY-376: {@code backupDirectory} is ignored */ - @Deprecated - public SecretRewriter(File backupDirectory) throws GeneralSecurityException { - this(); - } - - private String tryRewrite(String s) throws InvalidKeyException { - if (s.length() < 24) - return s; // Encrypting "" in Secret produces 24-letter characters, so this must be the minimum length - if (!isBase64(s)) - return s; // decode throws IOException if the input is not base64, and this is also a very quick way to filter - - byte[] in; - try { - in = Base64.getDecoder().decode(s.getBytes(StandardCharsets.UTF_8)); - } catch (IllegalArgumentException e) { - return s; // not a valid base64 - } - cipher.init(Cipher.DECRYPT_MODE, key); - Secret sec = HistoricalSecrets.tryDecrypt(cipher, in); - if (sec != null) // matched - return sec.getEncryptedValue(); // replace by the new encrypted value - else // not encrypted with the legacy key. leave it unmodified - return s; - } - - /** @deprecated SECURITY-376: {@code backup} is ignored */ - @Deprecated - public boolean rewrite(File f, File backup) throws InvalidKeyException, IOException { - return rewrite(f); - } - - public boolean rewrite(File f) throws InvalidKeyException, IOException { - - AtomicFileWriter w = new AtomicFileWriter(f.toPath(), StandardCharsets.UTF_8); - try { - boolean modified = false; // did we actually change anything? - try (PrintWriter out = new PrintWriter(new BufferedWriter(w)); - InputStream fin = Files.newInputStream(Util.fileToPath(f)); - BufferedReader r = new BufferedReader(new InputStreamReader(fin, StandardCharsets.UTF_8))) { - String line; - StringBuilder buf = new StringBuilder(); - - while ((line = r.readLine()) != null) { - int copied = 0; - buf.setLength(0); - while (true) { - int sidx = line.indexOf('>', copied); - if (sidx < 0) break; - int eidx = line.indexOf('<', sidx); - if (eidx < 0) break; - - String elementText = line.substring(sidx + 1, eidx); - String replacement = tryRewrite(elementText); - if (!replacement.equals(elementText)) - modified = true; - - buf.append(line, copied, sidx + 1); - buf.append(replacement); - copied = eidx; - } - buf.append(line.substring(copied)); - out.println(buf); - } - } - - if (modified) { - w.commit(); - } - return modified; - } finally { - w.abort(); - } - } - - - /** - * Recursively scans and rewrites a directory. - * - * This method shouldn't abort just because one file fails to rewrite. - * - * @return - * Number of files that were actually rewritten. - */ - // synchronized to prevent accidental concurrent use. this instance is not thread safe - public synchronized int rewriteRecursive(File dir, TaskListener listener) throws InvalidKeyException { - return rewriteRecursive(dir, "", listener); - } - - private int rewriteRecursive(File dir, String relative, TaskListener listener) throws InvalidKeyException { - String canonical; - try { - canonical = dir.toPath().toRealPath().toString(); - } catch (IOException | InvalidPathException e) { - canonical = dir.getAbsolutePath(); // - } - if (!callstack.add(canonical)) { - listener.getLogger().println("Cycle detected: " + dir); - return 0; - } - - try { - File[] children = dir.listFiles(); - if (children == null) return 0; - - int rewritten = 0; - for (File child : children) { - String cn = child.getName(); - if (cn.endsWith(".xml")) { - if (count++ % 100 == 0) - listener.getLogger().println("Scanning " + child); - try { - if (rewrite(child)) { - listener.getLogger().println("Rewritten " + child); - rewritten++; - } - } catch (IOException e) { - Functions.printStackTrace(e, listener.error("Failed to rewrite " + child)); - } - } - if (child.isDirectory()) { - if (!isIgnoredDir(child)) - rewritten += rewriteRecursive(child, - relative.isEmpty() ? cn : relative + '/' + cn, - listener); - } - } - return rewritten; - } finally { - callstack.remove(canonical); - } - } - - /** - * Decides if this directory is worth visiting or not. - */ - protected boolean isIgnoredDir(File dir) { - // ignoring the workspace and the artifacts directories. Both of them - // are potentially large and they do not store any secrets. - String n = dir.getName(); - return n.equals("workspace") || n.equals("artifacts") - || n.equals("plugins") // no mutable data here - || n.equals(".") || n.equals(".."); - } - - private static boolean isBase64(char ch) { - return ch < 128 && IS_BASE64[ch]; - } - - private static boolean isBase64(String s) { - for (int i = 0; i < s.length(); i++) - if (!isBase64(s.charAt(i))) - return false; - return true; - } - - private static final boolean[] IS_BASE64 = new boolean[128]; - - static { - String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - for (int i = 0; i < chars.length(); i++) - IS_BASE64[chars.charAt(i)] = true; - } -} diff --git a/core/src/main/java/hudson/util/StackedAreaRenderer2.java b/core/src/main/java/hudson/util/StackedAreaRenderer2.java index 3d165b27cce9..19fefcd65d9b 100644 --- a/core/src/main/java/hudson/util/StackedAreaRenderer2.java +++ b/core/src/main/java/hudson/util/StackedAreaRenderer2.java @@ -109,7 +109,7 @@ public void drawItem(Graphics2D g2, double value = dataValue.doubleValue(); - // leave the y values (y1, y0) untranslated as it is going to be be + // leave the y values (y1, y0) untranslated as it is going to be // stacked up later by previous series values, after this it will be // translated. double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java index cf1b5e9bcaab..774008e5e694 100644 --- a/core/src/main/java/hudson/util/XStream2.java +++ b/core/src/main/java/hudson/util/XStream2.java @@ -469,7 +469,7 @@ public Class realClass(String elementName) { */ private static final class AssociatedConverterImpl implements Converter { private final XStream xstream; - private static final ClassValue> classCache = new ClassValue>() { + private static final ClassValue> classCache = new ClassValue<>() { @Override protected Class computeValue(Class type) { return computeConverterClass(type); diff --git a/core/src/main/java/hudson/util/io/TarArchiver.java b/core/src/main/java/hudson/util/io/TarArchiver.java index 00b3d1b55d12..597b4ff47339 100644 --- a/core/src/main/java/hudson/util/io/TarArchiver.java +++ b/core/src/main/java/hudson/util/io/TarArchiver.java @@ -36,10 +36,10 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.attribute.BasicFileAttributes; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; -import org.apache.commons.compress.archivers.tar.TarConstants; -import org.apache.commons.compress.utils.BoundedInputStream; +import org.apache.commons.io.input.BoundedInputStream; +import org.apache.tools.tar.TarConstants; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarOutputStream; /** * {@link FileVisitor} that creates a tar archive. @@ -48,17 +48,17 @@ */ final class TarArchiver extends Archiver { private final byte[] buf = new byte[8192]; - private final TarArchiveOutputStream tar; + private final TarOutputStream tar; TarArchiver(OutputStream out, Charset filenamesEncoding) { - tar = new TarArchiveOutputStream(out, filenamesEncoding.name()); - tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR); - tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU); + tar = new TarOutputStream(out, filenamesEncoding.name()); + tar.setBigNumberMode(TarOutputStream.BIGNUMBER_STAR); + tar.setLongFileMode(TarOutputStream.LONGFILE_GNU); } @Override public void visitSymlink(File link, String target, String relativePath) throws IOException { - TarArchiveEntry e = new TarArchiveEntry(relativePath, TarConstants.LF_SYMLINK); + TarEntry e = new TarEntry(relativePath, TarConstants.LF_SYMLINK); try { int mode = IOUtils.mode(link); if (mode != -1) { @@ -70,8 +70,8 @@ public void visitSymlink(File link, String target, String relativePath) throws I e.setLinkName(target); - tar.putArchiveEntry(e); - tar.closeArchiveEntry(); + tar.putNextEntry(e); + tar.closeEntry(); entriesWritten++; } @@ -88,7 +88,7 @@ public void visit(File file, String relativePath) throws IOException { BasicFileAttributes basicFileAttributes = Files.readAttributes(Util.fileToPath(file), BasicFileAttributes.class); if (basicFileAttributes.isDirectory()) relativePath += '/'; - TarArchiveEntry te = new TarArchiveEntry(relativePath); + TarEntry te = new TarEntry(relativePath); int mode = IOUtils.mode(file); if (mode != -1) te.setMode(mode); te.setModTime(basicFileAttributes.lastModifiedTime().toMillis()); @@ -98,7 +98,7 @@ public void visit(File file, String relativePath) throws IOException { size = basicFileAttributes.size(); te.setSize(size); } - tar.putArchiveEntry(te); + tar.putNextEntry(te); try { if (!basicFileAttributes.isDirectory()) { // ensure we don't write more bytes than the declared when we created the entry @@ -118,7 +118,7 @@ public void visit(File file, String relativePath) throws IOException { } } } finally { // always close the entry - tar.closeArchiveEntry(); + tar.closeEntry(); } entriesWritten++; } diff --git a/core/src/main/java/hudson/views/ListViewColumn.java b/core/src/main/java/hudson/views/ListViewColumn.java index 1796f4cc495c..91e93e2145da 100644 --- a/core/src/main/java/hudson/views/ListViewColumn.java +++ b/core/src/main/java/hudson/views/ListViewColumn.java @@ -157,8 +157,7 @@ private static List createDefaultInitialColumnList(List d : descriptors) try { - if (d instanceof ListViewColumnDescriptor) { - ListViewColumnDescriptor ld = (ListViewColumnDescriptor) d; + if (d instanceof ListViewColumnDescriptor ld) { if (!ld.shownByDefault()) { continue; // skip this } diff --git a/core/src/main/java/jenkins/agents/CloudSet.java b/core/src/main/java/jenkins/agents/CloudSet.java index e69a3f9add3c..163556a001d1 100644 --- a/core/src/main/java/jenkins/agents/CloudSet.java +++ b/core/src/main/java/jenkins/agents/CloudSet.java @@ -41,7 +41,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.logging.Level; @@ -131,7 +130,7 @@ public String getCloudUpdateCenterCategoryLabel() { @Override public ModelObjectWithContextMenu.ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { ModelObjectWithContextMenu.ContextMenu m = new ModelObjectWithContextMenu.ContextMenu(); - Jenkins.get().clouds.stream().forEach(c -> m.add(c)); + Jenkins.get().clouds.stream().forEach(m::add); return m; } @@ -242,9 +241,13 @@ private void handleNewCloudPage(Descriptor descriptor, String name, Stapl */ @POST public synchronized void doDoCreate(StaplerRequest req, StaplerResponse rsp, - @QueryParameter String type) throws IOException, ServletException, Descriptor.FormException { + @QueryParameter String cloudDescriptorName) throws IOException, ServletException, Descriptor.FormException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); - Cloud cloud = Cloud.all().find(type).newInstance(req, req.getSubmittedForm()); + Descriptor cloudDescriptor = Cloud.all().findByName(cloudDescriptorName); + if (cloudDescriptor == null) { + throw new Failure(String.format("No cloud type ‘%s’ is known", cloudDescriptorName)); + } + Cloud cloud = cloudDescriptor.newInstance(req, req.getSubmittedForm()); if (!Jenkins.get().clouds.add(cloud)) { LOGGER.log(Level.WARNING, () -> "Creating duplicate cloud name " + cloud.name + ". Plugin " + Jenkins.get().getPluginManager().whichPlugin(cloud.getClass()) + " should be updated to support user provided name."); } @@ -261,7 +264,7 @@ public void doReorder(StaplerRequest req, StaplerResponse rsp) throws IOExceptio } var namesList = Arrays.asList(names); var clouds = new ArrayList<>(Jenkins.get().clouds); - Collections.sort(clouds, Comparator.comparingInt(c -> getIndexOf(namesList, c))); + clouds.sort(Comparator.comparingInt(c -> getIndexOf(namesList, c))); Jenkins.get().clouds.replaceBy(clouds); rsp.sendRedirect2("."); } diff --git a/core/src/main/java/jenkins/agents/WebSocketAgents.java b/core/src/main/java/jenkins/agents/WebSocketAgents.java index 005587c6365a..d9560f156ebd 100644 --- a/core/src/main/java/jenkins/agents/WebSocketAgents.java +++ b/core/src/main/java/jenkins/agents/WebSocketAgents.java @@ -36,6 +36,7 @@ import hudson.remoting.ChannelBuilder; import hudson.remoting.ChunkHeader; import hudson.remoting.Engine; +import hudson.slaves.SlaveComputer; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; @@ -107,7 +108,9 @@ public HttpResponse doIndex(StaplerRequest req, StaplerResponse rsp) throws IOEx Capability remoteCapability = Capability.fromASCII(remoteCapabilityStr); LOGGER.fine(() -> "received " + remoteCapability); rsp.setHeader(Capability.KEY, new Capability().toASCII()); - rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString()); + if (!SlaveComputer.ALLOW_UNSUPPORTED_REMOTING_VERSIONS) { + rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString()); + } rsp.setHeader(Engine.WEBSOCKET_COOKIE_HEADER, cookie); return WebSockets.upgrade(new Session(state, agent, remoteCapability)); } diff --git a/core/src/main/java/jenkins/diagnostics/URICheckEncodingMonitor.java b/core/src/main/java/jenkins/diagnostics/URICheckEncodingMonitor.java index 3563699b19b7..9fdd7bc38436 100644 --- a/core/src/main/java/jenkins/diagnostics/URICheckEncodingMonitor.java +++ b/core/src/main/java/jenkins/diagnostics/URICheckEncodingMonitor.java @@ -7,6 +7,7 @@ import hudson.model.AdministrativeMonitor; import hudson.util.FormValidation; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; @@ -22,7 +23,7 @@ public class URICheckEncodingMonitor extends AdministrativeMonitor { private static final Logger LOGGER = Logger.getLogger(URICheckEncodingMonitor.class.getName()); public boolean isCheckEnabled() { - return !"ISO-8859-1".equalsIgnoreCase(System.getProperty("file.encoding")); + return !"ISO-8859-1".equalsIgnoreCase(Charset.defaultCharset().displayName()); } @Override diff --git a/core/src/main/java/jenkins/fingerprints/FileFingerprintStorage.java b/core/src/main/java/jenkins/fingerprints/FileFingerprintStorage.java index d3154e6db6a3..409788ba5706 100644 --- a/core/src/main/java/jenkins/fingerprints/FileFingerprintStorage.java +++ b/core/src/main/java/jenkins/fingerprints/FileFingerprintStorage.java @@ -101,11 +101,10 @@ public FileFingerprintStorage() {} try { Object loaded = configFile.read(); - if (!(loaded instanceof Fingerprint)) { + if (!(loaded instanceof Fingerprint f)) { throw new IOException("Unexpected Fingerprint type. Expected " + Fingerprint.class + " or subclass but got " + (loaded != null ? loaded.getClass() : "null")); } - Fingerprint f = (Fingerprint) loaded; if (f.getPersistedFacets() == null) { logger.log(Level.WARNING, "Malformed fingerprint {0}: Missing facets", configFile); Files.deleteIfExists(Util.fileToPath(file)); diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index 3d1bf24e267d..6fb37e20bf3a 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -234,7 +234,7 @@ public static void saveLastExecVersion() { if (configFile.exists()) { try { String lastVersion = XMLUtils.getValue("/hudson/version", configFile); - if (lastVersion.length() > 0) { + if (!lastVersion.isEmpty()) { LOGGER.log(Level.FINE, "discovered serialized lastVersion {0}", lastVersion); return lastVersion; } diff --git a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java index cc1d5214e0ae..5b94090f4e3b 100644 --- a/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java +++ b/core/src/main/java/jenkins/management/AdministrativeMonitorsDecorator.java @@ -139,7 +139,7 @@ private Collection getAllActiveAdministrativeMonitors() { * @return the list of active monitors if we should display them, otherwise null. */ public Collection getMonitorsToDisplay() { - if (!Jenkins.get().hasPermission(Jenkins.SYSTEM_READ)) { + if (!(AdministrativeMonitor.hasPermissionToDisplay())) { return null; } @@ -150,7 +150,7 @@ public Collection getMonitorsToDisplay() { } List ancestors = req.getAncestors(); - if (ancestors == null || ancestors.size() == 0) { + if (ancestors == null || ancestors.isEmpty()) { // ??? return null; } diff --git a/core/src/main/java/jenkins/management/AsynchronousAdministrativeMonitor.java b/core/src/main/java/jenkins/management/AsynchronousAdministrativeMonitor.java index 7ebc732518ae..2884439681c6 100644 --- a/core/src/main/java/jenkins/management/AsynchronousAdministrativeMonitor.java +++ b/core/src/main/java/jenkins/management/AsynchronousAdministrativeMonitor.java @@ -17,7 +17,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; -import jenkins.security.RekeySecretAdminMonitor; /** * Convenient partial implementation of {@link AdministrativeMonitor} that involves a background "fixing" action @@ -27,9 +26,6 @@ * A subclass defines what that background fixing actually does in {@link #fix(TaskListener)}. The logging output * from it gets persisted, and this class provides a "/log" view that allows the administrator to monitor its progress. * - *

- * See {@link RekeySecretAdminMonitor} for an example of how to subtype this class. - * * @author Kohsuke Kawaguchi */ public abstract class AsynchronousAdministrativeMonitor extends AdministrativeMonitor { diff --git a/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java b/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java new file mode 100644 index 000000000000..46d760fe8ee2 --- /dev/null +++ b/core/src/main/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfiguration.java @@ -0,0 +1,72 @@ +package jenkins.model; + +import hudson.Extension; +import hudson.model.PersistentDescriptor; +import java.util.logging.Logger; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.StaplerRequest; + +/** + * Configures check interval for computer retention. + * + * @author Jakob Ackermann + */ +@Extension(ordinal = 401) @Symbol("computerRetentionCheckInterval") +public class GlobalComputerRetentionCheckIntervalConfiguration extends GlobalConfiguration implements PersistentDescriptor { + /** + * For {@link hudson.slaves.ComputerRetentionWork#getRecurrencePeriod()} + */ + private int computerRetentionCheckInterval = 60; + + /** + * Gets the check interval for computer retention. + * + * @since 2.463 + */ + public int getComputerRetentionCheckInterval() { + if (computerRetentionCheckInterval <= 0) { + LOGGER.info("computerRetentionCheckInterval must be greater than zero, falling back to 60s"); + return 60; + } + if (computerRetentionCheckInterval > 60) { + LOGGER.info("computerRetentionCheckInterval is limited to 60s"); + return 60; + } + return computerRetentionCheckInterval; + } + + /** + * Updates the check interval for computer retention and restarts the check cycle. + * + * @param interval new check interval in seconds + * @throws IllegalArgumentException interval must be greater than zero + * @since 2.463 + */ + private void setComputerRetentionCheckInterval(int interval) throws IllegalArgumentException { + if (interval <= 0) { + throw new IllegalArgumentException("interval must be greater than zero"); + } + if (interval > 60) { + throw new IllegalArgumentException("interval must be below or equal 60s"); + } + computerRetentionCheckInterval = interval; + save(); + } + + @Override + public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + try { + final int interval = json.getInt("computerRetentionCheckInterval"); + setComputerRetentionCheckInterval(interval); + return true; + } catch (IllegalArgumentException e) { + throw new FormException(e, "computerRetentionCheckInterval"); + } catch (JSONException e) { + throw new FormException(e.getMessage(), "computerRetentionCheckInterval"); + } + } + + private static final Logger LOGGER = Logger.getLogger(GlobalComputerRetentionCheckIntervalConfiguration.class.getName()); +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 77e6f69ec875..a648935527ae 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -183,6 +183,7 @@ import hudson.triggers.TriggerDescriptor; import hudson.util.AdministrativeError; import hudson.util.ClockDifference; +import hudson.util.ComboBoxModel; import hudson.util.CopyOnWriteList; import hudson.util.CopyOnWriteMap; import hudson.util.DaemonThreadFactory; @@ -499,7 +500,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * STARTUP_MARKER_FILE.get(); // returns false if we are on a fresh startup. True for next startups. * } */ - private static transient FileBoolean STARTUP_MARKER_FILE; + private static FileBoolean STARTUP_MARKER_FILE; private volatile List jdks = new ArrayList<>(); @@ -1908,6 +1909,11 @@ public Collection getJobNames() { return names; } + @Restricted(NoExternalUse.class) + public ComboBoxModel doFillJobNameItems() { + return new ComboBoxModel(getJobNames()); + } + @Override public List getViewActions() { return getActions(); @@ -2355,12 +2361,12 @@ public AdministrativeMonitor getAdministrativeMonitor(String id) { * @since 2.64 */ public List getActiveAdministrativeMonitors() { - if (!Jenkins.get().hasPermission(SYSTEM_READ)) { + if (!AdministrativeMonitor.hasPermissionToDisplay()) { return Collections.emptyList(); } return administrativeMonitors.stream().filter(m -> { try { - return Jenkins.get().hasPermission(m.getRequiredPermission()) && m.isEnabled() && m.isActivated(); + return m.hasRequiredPermission() && m.isEnabled() && m.isActivated(); } catch (Throwable x) { LOGGER.log(Level.WARNING, null, x); return false; @@ -2897,13 +2903,16 @@ public ExtensionList getExtensionList(String extensionType) throws ClassNotFound */ public void refreshExtensions() throws ExtensionRefreshException { ExtensionList finders = getExtensionList(ExtensionFinder.class); + LOGGER.finer(() -> "refreshExtensions " + finders); for (ExtensionFinder ef : finders) { if (!ef.isRefreshable()) throw new ExtensionRefreshException(ef + " doesn't support refresh"); } List fragments = new ArrayList<>(); + for (ExtensionFinder ef : finders) { + LOGGER.finer(() -> "searching " + ef); fragments.add(ef.refresh()); } ExtensionComponentSet delta = ExtensionComponentSet.union(fragments).filtered(); @@ -2912,12 +2921,21 @@ public void refreshExtensions() throws ExtensionRefreshException { List> newFinders = new ArrayList<>(delta.find(ExtensionFinder.class)); while (!newFinders.isEmpty()) { ExtensionFinder f = newFinders.remove(newFinders.size() - 1).getInstance(); + LOGGER.finer(() -> "found new ExtensionFinder " + f); ExtensionComponentSet ecs = ExtensionComponentSet.allOf(f).filtered(); newFinders.addAll(ecs.find(ExtensionFinder.class)); delta = ExtensionComponentSet.union(delta, ecs); } + // we may not have found a new Extension finder but we may be using an extension finder that is extensible + // e.g. hudson.ExtensionFinder.GuiceFinder is extensible by GuiceExtensionAnnotation which is done by the variant plugin + // so lets give it one more chance. + for (ExtensionFinder ef : finders) { + LOGGER.finer(() -> "searching again in " + ef); + delta = ExtensionComponentSet.union(delta, ef.refresh().filtered()); + } + for (ExtensionList el : extensionLists.values()) { el.refresh(delta); } @@ -3094,8 +3112,7 @@ public Item getItem(String pathName, ItemGroup context) { continue; } - if (ctx instanceof ItemGroup) { - ItemGroup g = (ItemGroup) ctx; + if (ctx instanceof ItemGroup g) { Item i = g.getItem(s); if (i == null || !i.hasPermission(Item.READ)) { // TODO consider DISCOVER ctx = null; // can't go up further @@ -3465,10 +3482,9 @@ private void setBuildsAndWorkspacesDir() throws IOException, InvalidBuildsDir { 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()) { + do { d = d.getParentFile(); - } + } while (!d.exists()); if (!d.canWrite()) { throw new InvalidBuildsDir(newBuildsDirValue + " does not exist and probably cannot be created"); } @@ -4480,7 +4496,7 @@ public void doDoFingerprintCheck(StaplerRequest req, StaplerResponse rsp) throws rsp.sendError(HttpServletResponse.SC_FORBIDDEN, "No crumb found"); } rsp.sendRedirect2(req.getContextPath() + "/fingerprint/" + - Util.getDigestOf(p.getFileItem("name").getInputStream()) + '/'); + Util.getDigestOf(p.getFileItem2("name").getInputStream()) + '/'); } } diff --git a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java index be71a97c7379..662ffa9359e2 100644 --- a/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java +++ b/core/src/main/java/jenkins/model/ParameterizedJobMixIn.java @@ -218,6 +218,7 @@ public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParamet Queue.Item item = Jenkins.get().getQueue().schedule2(asJob(), delay.getTimeInSeconds(), getBuildCause(asJob(), req)).getItem(); if (item != null) { + // TODO JENKINS-66105 use SC_SEE_OTHER if !ScheduleResult.created rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl()); } else { rsp.sendRedirect("."); @@ -346,7 +347,7 @@ static ParameterizedJob resolveForCLI(@Argument(required = true, metaVar = "NAME * (Would have been done entirely as an interface with default methods had this been designed for Java 8.) */ default ParameterizedJobMixIn getParameterizedJobMixIn() { - return new ParameterizedJobMixIn() { + return new ParameterizedJobMixIn<>() { @SuppressWarnings("unchecked") // untypable @Override protected JobT asJob() { return (JobT) ParameterizedJob.this; diff --git a/test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/compress.groovy b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java similarity index 60% rename from test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/compress.groovy rename to core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java index 2c342e42bf97..e8f8dcc31775 100644 --- a/test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Groovy/compress.groovy +++ b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2019 CloudBees, Inc. + * Copyright (c) 2024, Markus Winter * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,13 +22,28 @@ * THE SOFTWARE. */ -package jenkins.security.stapler.StaplerDispatchValidatorTest.Groovy +package jenkins.model.experimentalflags; -def st = namespace('jelly:stapler') -def l = namespace('/lib/layout') -st.compress { - l.view { - st.contentType(value: 'text/html') - st.include(page: 'frag') +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Extension +@Restricted(NoExternalUse.class) +public class RemoveYuiUserExperimentalFlag extends BooleanUserExperimentalFlag { + public RemoveYuiUserExperimentalFlag() { + super("remove-yui.flag"); + } + + @Override + public String getDisplayName() { + return "Remove YUI"; + } + + @Nullable + @Override + public String getShortDescription() { + return "Remove YUI from all Jenkins UI pages. This will break anything that depends on YUI"; } } diff --git a/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java b/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java index 6732f2e5d696..85332d26973c 100644 --- a/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java +++ b/core/src/main/java/jenkins/model/experimentalflags/UserExperimentalFlagsProperty.java @@ -31,6 +31,7 @@ import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; +import hudson.model.userproperty.UserPropertyCategory; import java.util.HashMap; import java.util.Map; import net.sf.json.JSONObject; @@ -76,13 +77,19 @@ public static final class DescriptorImpl extends UserPropertyDescriptor { public UserProperty newInstance(@Nullable StaplerRequest req, @NonNull JSONObject formData) throws FormException { JSONObject flagsObj = formData.getJSONObject("flags"); Map flags = new HashMap<>(); - for (Object key : flagsObj.keySet()) { - String value = (String) flagsObj.get((String) key); + for (String key : flagsObj.keySet()) { + String value = (String) flagsObj.get(key); if (!value.isEmpty()) { - flags.put((String) key, value); + flags.put(key, value); } } return new UserExperimentalFlagsProperty(flags); } + + @NonNull + @Override + public UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Experimental.class); + } } } diff --git a/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java b/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java index 570048c5327f..c73c5cbf80f1 100644 --- a/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java +++ b/core/src/main/java/jenkins/model/lazy/LazyBuildMixIn.java @@ -39,7 +39,6 @@ import hudson.model.RunMap; import hudson.model.listeners.ItemListener; import hudson.model.queue.SubTask; -import hudson.widgets.BuildHistoryWidget; import hudson.widgets.HistoryWidget; import java.io.File; import java.io.IOException; @@ -297,10 +296,11 @@ public List getEstimatedDurationCandidates() { } /** - * Suitable for {@link Job#createHistoryWidget}. + * @deprecated Remove any code calling this method, history widget is now created via {@link jenkins.widgets.WidgetFactory} implementation. */ + @Deprecated(forRemoval = true, since = "2.459") public final HistoryWidget createHistoryWidget() { - return new BuildHistoryWidget(asJob(), builds, Job.HISTORY_ADAPTER); + throw new IllegalStateException("HistoryWidget is now created via WidgetFactory implementation"); } /** diff --git a/core/src/main/java/jenkins/model/queue/ItemDeletion.java b/core/src/main/java/jenkins/model/queue/ItemDeletion.java index e5c9fc2ca87e..a2d954fbc459 100644 --- a/core/src/main/java/jenkins/model/queue/ItemDeletion.java +++ b/core/src/main/java/jenkins/model/queue/ItemDeletion.java @@ -28,25 +28,42 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.ExtensionList; +import hudson.model.AbstractItem; import hudson.model.Action; +import hudson.model.Computer; +import hudson.model.Executor; +import hudson.model.Failure; import hudson.model.Item; +import hudson.model.Messages; import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.queue.Executables; +import hudson.model.queue.SubTask; import hudson.model.queue.Tasks; +import hudson.model.queue.WorkUnit; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Logger; +import jenkins.model.Jenkins; import net.jcip.annotations.GuardedBy; /** * A {@link Queue.QueueDecisionHandler} that blocks items being deleted from entering the queue. - * + * @see AbstractItem#delete() * @since 2.55 */ @Extension public class ItemDeletion extends Queue.QueueDecisionHandler { + private static final Logger LOGGER = Logger.getLogger(ItemDeletion.class.getName()); + /** * Lock to guard the {@link #registrations} set. */ @@ -176,4 +193,94 @@ public boolean shouldSchedule(Queue.Task p, List actions) { } return true; } + + /** + * Cancels any builds in progress of this item (if a job) or descendants (if a folder). + * Also cancels any associated queue items. + * @param initiatingItem an item being deleted + * @since 2.470 + */ + public static void cancelBuildsInProgress(@NonNull Item initiatingItem) throws Failure, InterruptedException { + Queue queue = Queue.getInstance(); + if (initiatingItem instanceof Queue.Task) { + // clear any items in the queue so they do not get picked up + queue.cancel((Queue.Task) initiatingItem); + } + // now cancel any child items - this happens after ItemDeletion registration, so we can use a snapshot + for (Queue.Item i : queue.getItems()) { + Item item = Tasks.getItemOf(i.task); + while (item != null) { + if (item == initiatingItem) { + if (!queue.cancel(i)) { + LOGGER.warning(() -> "failed to cancel " + i); + } + break; + } + if (item.getParent() instanceof Item) { + item = (Item) item.getParent(); + } else { + break; + } + } + } + // interrupt any builds in progress (and this should be a recursive test so that folders do not pay + // the 15 second delay for every child item). This happens after queue cancellation, so will be + // a complete set of builds in flight + Map buildsInProgress = new LinkedHashMap<>(); + for (Computer c : Jenkins.get().getComputers()) { + for (Executor e : c.getAllExecutors()) { + final WorkUnit workUnit = e.getCurrentWorkUnit(); + final Queue.Executable executable = workUnit != null ? workUnit.getExecutable() : null; + final SubTask subtask = executable != null ? Executables.getParentOf(executable) : null; + if (subtask != null) { + Item item = Tasks.getItemOf(subtask); + while (item != null) { + if (item == initiatingItem) { + buildsInProgress.put(e, e.getCurrentExecutable()); + e.interrupt(Result.ABORTED); + break; + } + if (item.getParent() instanceof Item) { + item = (Item) item.getParent(); + } else { + break; + } + } + } + } + } + if (!buildsInProgress.isEmpty()) { + // give them 15 seconds or so to respond to the interrupt + long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); + // comparison with executor.getCurrentExecutable() == computation currently should always be true + // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling + while (!buildsInProgress.isEmpty() && expiration - System.nanoTime() > 0L) { + // we know that ItemDeletion will prevent any new builds in the queue + // ItemDeletion happens-before Queue.cancel so we know that the Queue will stay clear + // Queue.cancel happens-before collecting the buildsInProgress list + // thus buildsInProgress contains the complete set we need to interrupt and wait for + for (Iterator> iterator = + buildsInProgress.entrySet().iterator(); + iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + // comparison with executor.getCurrentExecutable() == executable currently should always be + // true as we no longer recycle Executors, but safer to future-proof in case we ever + // revisit recycling. + if (!entry.getKey().isAlive() + || entry.getValue() != entry.getKey().getCurrentExecutable()) { + iterator.remove(); + } + // I don't know why, but we have to keep interrupting + entry.getKey().interrupt(Result.ABORTED); + } + Thread.sleep(50L); + } + if (!buildsInProgress.isEmpty()) { + throw new Failure(Messages.AbstractItem_FailureToStopBuilds( + buildsInProgress.size(), initiatingItem.getFullDisplayName() + )); + } + } + } + } diff --git a/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java b/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java index 86093a634617..c95740613765 100644 --- a/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java +++ b/core/src/main/java/jenkins/monitor/JavaVersionRecommendationAdminMonitor.java @@ -78,7 +78,6 @@ public class JavaVersionRecommendationAdminMonitor extends AdministrativeMonitor static { NavigableMap supportedVersions = new TreeMap<>(); - supportedVersions.put(11, LocalDate.of(2024, 9, 30)); // Temurin: 2024-10-31 supportedVersions.put(17, LocalDate.of(2026, 3, 31)); // Temurin: 2027-10-31 supportedVersions.put(21, LocalDate.of(2027, 9, 30)); // Temurin: 2029-09-30 SUPPORTED_JAVA_VERSIONS = Collections.unmodifiableNavigableMap(supportedVersions); 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 index 40c0eae1d13f..555d0c58f5e3 100644 --- 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 @@ -420,7 +420,7 @@ protected boolean isValidAuthority(String authority) { } } String port = authorityMatcher.group(PARSE_AUTHORITY_PORT); - if (port != null && port.length() > 0) { + if (port != null && !port.isEmpty()) { try { int iPort = Integer.parseInt(port); if (iPort < 0 || iPort > MAX_UNSIGNED_16_BIT_INT) { @@ -433,7 +433,7 @@ protected boolean isValidAuthority(String authority) { } String extra = authorityMatcher.group(PARSE_AUTHORITY_EXTRA); - if (extra != null && extra.trim().length() > 0) { + if (extra != null && !extra.trim().isEmpty()) { return false; } diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java index 69904a0e4785..464fdcbdf16c 100644 --- a/core/src/main/java/jenkins/security/ApiTokenProperty.java +++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java @@ -33,6 +33,7 @@ import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; +import hudson.model.userproperty.UserPropertyCategory; import hudson.security.ACL; import hudson.util.HttpResponses; import hudson.util.Secret; @@ -657,6 +658,11 @@ public HttpResponse doRevokeAllExcept(@AncestorInPath User u, return HttpResponses.ok(); } + + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Security.class); + } } /** diff --git a/core/src/main/java/jenkins/security/BasicHeaderProcessor.java b/core/src/main/java/jenkins/security/BasicHeaderProcessor.java index 18ea7b5174c0..45b5eb0c70a2 100644 --- a/core/src/main/java/jenkins/security/BasicHeaderProcessor.java +++ b/core/src/main/java/jenkins/security/BasicHeaderProcessor.java @@ -9,6 +9,7 @@ import hudson.util.Scrambler; import java.io.IOException; import java.util.List; +import java.util.Locale; import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -64,7 +65,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletResponse rsp = (HttpServletResponse) response; String authorization = req.getHeader("Authorization"); - if (authorization != null && authorization.toLowerCase().startsWith("Basic ".toLowerCase())) { + if (authorization != null && authorization.toLowerCase(Locale.ROOT).startsWith("Basic ".toLowerCase(Locale.ROOT))) { // authenticate the user String uidpassword = Scrambler.descramble(authorization.substring(6)); int idx = uidpassword.indexOf(':'); diff --git a/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java b/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java index 52f74d452ffa..cb8b8accf295 100644 --- a/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java +++ b/core/src/main/java/jenkins/security/LastGrantedAuthoritiesProperty.java @@ -6,6 +6,7 @@ import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; +import hudson.model.userproperty.UserPropertyCategory; import hudson.security.SecurityRealm; import java.io.IOException; import java.util.ArrayList; @@ -171,6 +172,11 @@ public boolean isEnabled() { public UserProperty newInstance(User user) { return null; } + + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Invisible.class); + } } private static final Logger LOGGER = Logger.getLogger(LastGrantedAuthoritiesProperty.class.getName()); diff --git a/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java b/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java index 733a6c89a1f2..1e201bcd3dbc 100644 --- a/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java +++ b/core/src/main/java/jenkins/security/RedactSecretJsonInErrorMessageSanitizer.java @@ -88,7 +88,6 @@ private Object copyAndSanitize(Object value) { } } - @SuppressWarnings("unchecked") private JSONObject copyAndSanitizeObject(JSONObject jsonObject) { Set redactedKeySet = retrieveRedactedKeys(jsonObject); JSONObject result = new JSONObject(); diff --git a/core/src/main/java/jenkins/security/RekeySecretAdminMonitor.java b/core/src/main/java/jenkins/security/RekeySecretAdminMonitor.java deleted file mode 100644 index c3b548004515..000000000000 --- a/core/src/main/java/jenkins/security/RekeySecretAdminMonitor.java +++ /dev/null @@ -1,160 +0,0 @@ -package jenkins.security; - -import hudson.Extension; -import hudson.Functions; -import hudson.Util; -import hudson.init.InitMilestone; -import hudson.init.Initializer; -import hudson.model.TaskListener; -import hudson.util.HttpResponses; -import hudson.util.SecretRewriter; -import hudson.util.VersionNumber; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.security.GeneralSecurityException; -import java.util.Date; -import java.util.logging.Level; -import java.util.logging.Logger; -import jenkins.management.AsynchronousAdministrativeMonitor; -import jenkins.model.Jenkins; -import jenkins.util.io.FileBoolean; -import org.jenkinsci.Symbol; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.interceptor.RequirePOST; - -/** - * Warns the administrator to run {@link SecretRewriter} - * - * @author Kohsuke Kawaguchi - */ -@Extension @Symbol("rekeySecret") -public class RekeySecretAdminMonitor extends AsynchronousAdministrativeMonitor { - - /** - * Whether we detected a need to run the rewrite program. - * Once we set it to true, we'll never turn it off. - * - * If the admin decides to dismiss this warning, we use {@link #isEnabled()} for that. - * - * In this way we can correctly differentiate all the different states. - */ - private final FileBoolean needed = state("needed"); - - /** - * If the scanning process has run to the completion, we set to this true. - */ - private final FileBoolean done = state("done"); - - /** - * If the rewrite process is scheduled upon the next boot. - */ - private final FileBoolean scanOnBoot = state("scanOnBoot"); - - @SuppressWarnings("OverridableMethodCallInConstructor") // should have been final - public RekeySecretAdminMonitor() throws IOException { - // if JENKINS_HOME existed <1.497, we need to offer rewrite - // this computation needs to be done and the value be captured, - // since $JENKINS_HOME/config.xml can be saved later before the user has - // actually rewritten XML files. - Jenkins j = Jenkins.get(); - if (j.isUpgradedFromBefore(new VersionNumber("1.496.*")) - && new FileBoolean(new File(j.getRootDir(), "secret.key.not-so-secret")).isOff()) - needed.on(); - Util.deleteRecursive(new File(getBaseDir(), "backups")); // SECURITY-376: no longer used - } - - @Override - public boolean isActivated() { - return needed.isOn(); - } - - /** - * Indicates that the re-keying has run to the completion. - */ - public boolean isDone() { - return done.isOn(); - } - - public void setNeeded() { - needed.on(); - } - - public boolean isScanOnBoot() { - return scanOnBoot.isOn(); - } - - @Override - public boolean isSecurity() { - return true; - } - - @RequirePOST - public HttpResponse doScan(StaplerRequest req) throws IOException, GeneralSecurityException { - if (req.hasParameter("background")) { - start(false); - } else - if (req.hasParameter("schedule")) { - scanOnBoot.on(); - } else - if (req.hasParameter("dismiss")) { - disable(true); - } else - throw HttpResponses.error(400, "Invalid request submission: " + req.getParameterMap()); - - return HttpResponses.redirectViaContextPath("/manage"); - } - - - private FileBoolean state(String name) { - return new FileBoolean(new File(getBaseDir(), name)); - } - - @Initializer(fatal = false, after = InitMilestone.PLUGINS_STARTED, before = InitMilestone.EXTENSIONS_AUGMENTED) - // as early as possible, but this needs to be late enough that the ConfidentialStore is available - public void scanOnReboot() throws InterruptedException, IOException, GeneralSecurityException { - FileBoolean flag = scanOnBoot; - if (flag.isOn()) { - flag.off(); - start(false).join(); - // block the boot until the rewrite process is complete - // don't let the failure in RekeyThread block Jenkins boot. - } - } - - @Override - public String getDisplayName() { - return Messages.RekeySecretAdminMonitor_DisplayName(); - } - - /** - * Rewrite log file. - */ - @Override - protected File getLogFile() { - return new File(getBaseDir(), "rekey.log"); - } - - @Override - protected void fix(TaskListener listener) throws Exception { - LOGGER.info("Initiating a re-keying of secrets. See " + getLogFile()); - - SecretRewriter rewriter = new SecretRewriter(); - - try { - PrintStream log = listener.getLogger(); - log.println("Started re-keying " + new Date()); - int count = rewriter.rewriteRecursive(Jenkins.get().getRootDir(), listener); - log.printf("Completed re-keying %d files on %s%n", count, new Date()); - new RekeySecretAdminMonitor().done.on(); - LOGGER.info("Secret re-keying completed"); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Fatal failure in re-keying secrets", e); - Functions.printStackTrace(e, listener.error("Fatal failure in rewriting secrets")); - } - } - - private static final Logger LOGGER = Logger.getLogger(RekeySecretAdminMonitor.class.getName()); - -} diff --git a/core/src/main/java/jenkins/security/ResourceDomainConfiguration.java b/core/src/main/java/jenkins/security/ResourceDomainConfiguration.java index 5c569ce40d50..cd552fc65130 100644 --- a/core/src/main/java/jenkins/security/ResourceDomainConfiguration.java +++ b/core/src/main/java/jenkins/security/ResourceDomainConfiguration.java @@ -150,8 +150,7 @@ private FormValidation checkUrl(String resourceRootUrlString, boolean allowOnlin // Send a request to /instance-identity/ at the resource root URL and check whether it is this Jenkins try { URLConnection urlConnection = new URI(resourceRootUrlString + "instance-identity/").toURL().openConnection(); - if (urlConnection instanceof HttpURLConnection) { - HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection; + if (urlConnection instanceof HttpURLConnection httpURLConnection) { int responseCode = httpURLConnection.getResponseCode(); if (responseCode == 200) { diff --git a/core/src/main/java/jenkins/security/s2m/JarURLValidatorImpl.java b/core/src/main/java/jenkins/security/s2m/JarURLValidatorImpl.java new file mode 100644 index 000000000000..7fafcea946d5 --- /dev/null +++ b/core/src/main/java/jenkins/security/s2m/JarURLValidatorImpl.java @@ -0,0 +1,104 @@ +/* + * The MIT License + * + * Copyright (c) 2024 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.s2m; + +import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; +import hudson.PluginManager; +import hudson.remoting.Channel; +import hudson.remoting.ChannelBuilder; +import hudson.remoting.JarURLValidator; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import jenkins.security.ChannelConfigurator; +import jenkins.util.SystemProperties; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +@Deprecated +@Extension +public class JarURLValidatorImpl extends ChannelConfigurator implements JarURLValidator { + + public static final Logger LOGGER = Logger.getLogger(JarURLValidatorImpl.class.getName()); + + @Override + public void onChannelBuilding(ChannelBuilder builder, @Nullable Object context) { + LOGGER.log(Level.CONFIG, () -> "Setting up JarURLValidatorImpl for context: " + context); + builder.withProperty(JarURLValidator.class, this); + } + + @Override + public void validate(URL url) throws IOException { + final String rejectAllProp = JarURLValidatorImpl.class.getName() + ".REJECT_ALL"; + if (SystemProperties.getBoolean(rejectAllProp)) { + LOGGER.log(Level.FINE, () -> "Rejecting URL due to configuration: " + url); + throw new IOException("The system property '" + rejectAllProp + "' has been set, so all attempts by agents to load jars from the controller are rejected." + + " Update the agent.jar of the affected agent to a version released in August 2024 or later to prevent this error."); // TODO better version spec + } + final String allowAllProp = Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR"; + if (SystemProperties.getBoolean(allowAllProp)) { + LOGGER.log(Level.FINE, () -> "Allowing URL due to configuration: " + url); + return; + } + if (!isAllowedJar(url)) { + LOGGER.log(Level.FINE, () -> "Rejecting URL: " + url); + throw new IOException("This URL does not point to a jar file allowed to be requested by agents: " + url + "." + + " Update the agent.jar of the affected agent to a version released in August 2024 or later to prevent this error." + + " Alternatively, set the system property '" + allowAllProp + "' to 'true' if all the code built by Jenkins is as trusted as an administrator."); + } else { + LOGGER.log(Level.FINE, () -> "Allowing URL: " + url); + } + } + @SuppressFBWarnings( + value = "DMI_COLLECTION_OF_URLS", + justification = "All URLs point to local files, so no DNS lookup.") + private static boolean isAllowedJar(URL url) { + final ClassLoader classLoader = Jenkins.get().getPluginManager().uberClassLoader; + if (classLoader instanceof PluginManager.UberClassLoader uberClassLoader) { + if (uberClassLoader.isPluginJar(url)) { + LOGGER.log(Level.FINER, () -> "Determined to be plugin jar: " + url); + return true; + } + } + + final ClassLoader coreClassLoader = Jenkins.class.getClassLoader(); + if (coreClassLoader instanceof URLClassLoader urlClassLoader) { + if (Set.of(urlClassLoader.getURLs()).contains(url)) { + LOGGER.log(Level.FINER, () -> "Determined to be core jar: " + url); + return true; + } + } + + LOGGER.log(Level.FINER, () -> "Neither core nor plugin jar: " + url); + return false; + } +} diff --git a/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java b/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java index 02753addb79d..e87506315a41 100644 --- a/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java +++ b/core/src/main/java/jenkins/security/seed/UserSeedChangeListener.java @@ -35,7 +35,7 @@ /** * Listener notified when a user was requested to changed their seed - * @since 2.160 and 2.150.2, but restricted (unavailable to plugins) before TODO + * @since 2.160 and 2.150.2, but restricted (unavailable to plugins) before 2.406 */ public abstract class UserSeedChangeListener implements ExtensionPoint { private static final Logger LOGGER = Logger.getLogger(SecurityListener.class.getName()); diff --git a/core/src/main/java/jenkins/security/seed/UserSeedProperty.java b/core/src/main/java/jenkins/security/seed/UserSeedProperty.java index d7420910cb64..968ee9320f58 100644 --- a/core/src/main/java/jenkins/security/seed/UserSeedProperty.java +++ b/core/src/main/java/jenkins/security/seed/UserSeedProperty.java @@ -31,6 +31,7 @@ import hudson.model.User; import hudson.model.UserProperty; import hudson.model.UserPropertyDescriptor; +import hudson.model.userproperty.UserPropertyCategory; import hudson.util.HttpResponses; import java.io.IOException; import java.security.SecureRandom; @@ -153,5 +154,10 @@ public synchronized HttpResponse doRenewSessionSeed(@AncestorInPath @NonNull Use public boolean isEnabled() { return !DISABLE_USER_SEED && !HIDE_USER_SEED_SECTION; } + + @Override + public @NonNull UserPropertyCategory getUserPropertyCategory() { + return UserPropertyCategory.get(UserPropertyCategory.Security.class); + } } } diff --git a/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java b/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java index 46b7cab9028b..cc658c275fcf 100644 --- a/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java +++ b/core/src/main/java/jenkins/security/stapler/StaplerFilteredActionListener.java @@ -50,7 +50,7 @@ public class StaplerFilteredActionListener implements FilteredDoActionTriggerLis @Override public boolean onDoActionTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node) { - LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{ + LOGGER.log(Level.FINER, LOG_MESSAGE, new Object[]{ req.getPathInfo(), f.getSignature(), }); @@ -59,7 +59,7 @@ public boolean onDoActionTrigger(Function f, StaplerRequest req, StaplerResponse @Override public boolean onGetterTrigger(Function f, StaplerRequest req, StaplerResponse rsp, Object node, String expression) { - LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{ + LOGGER.log(Level.FINER, LOG_MESSAGE, new Object[]{ req.getPathInfo(), f.getSignature(), }); @@ -68,7 +68,7 @@ public boolean onGetterTrigger(Function f, StaplerRequest req, StaplerResponse r @Override public boolean onFieldTrigger(FieldRef f, StaplerRequest req, StaplerResponse staplerResponse, Object node, String expression) { - LOGGER.log(Level.WARNING, LOG_MESSAGE, new Object[]{ + LOGGER.log(Level.FINER, LOG_MESSAGE, new Object[]{ req.getPathInfo(), f.getSignature(), }); @@ -77,7 +77,7 @@ public boolean onFieldTrigger(FieldRef f, StaplerRequest req, StaplerResponse st @Override public boolean onDispatchTrigger(StaplerRequest req, StaplerResponse rsp, Object node, String viewName) { - LOGGER.warning(() -> "New Stapler dispatch rules result in the URL \"" + req.getPathInfo() + "\" no longer being allowed. " + + LOGGER.finer(() -> "New Stapler dispatch rules result in the URL \"" + req.getPathInfo() + "\" no longer being allowed. " + "If you consider it safe to use, add the following to the whitelist: \"" + node.getClass().getName() + " " + viewName + "\". " + "Learn more: https://www.jenkins.io/redirect/stapler-facet-restrictions"); return false; diff --git a/core/src/main/java/jenkins/util/TreeString.java b/core/src/main/java/jenkins/util/TreeString.java index 3bcdc95d90ca..58890d225893 100644 --- a/core/src/main/java/jenkins/util/TreeString.java +++ b/core/src/main/java/jenkins/util/TreeString.java @@ -64,7 +64,7 @@ public final class TreeString implements Serializable { } /* package */TreeString(final TreeString parent, final String label) { - assert parent == null || label.length() > 0; // if there's a parent, + assert parent == null || !label.isEmpty(); // if there's a parent, // label can't be empty. this.parent = parent; diff --git a/core/src/main/java/jenkins/util/URLClassLoader2.java b/core/src/main/java/jenkins/util/URLClassLoader2.java index 72611bc49432..a6e5194e14f3 100644 --- a/core/src/main/java/jenkins/util/URLClassLoader2.java +++ b/core/src/main/java/jenkins/util/URLClassLoader2.java @@ -17,14 +17,43 @@ public class URLClassLoader2 extends URLClassLoader implements JenkinsClassLoade registerAsParallelCapable(); } + /** + * @deprecated use {@link URLClassLoader2#URLClassLoader2(String, URL[])} + */ + @Deprecated(since = "2.459") public URLClassLoader2(URL[] urls) { super(urls); } + /** + * @deprecated use {@link URLClassLoader2#URLClassLoader2(String, URL[], ClassLoader)} + */ + @Deprecated(since = "2.459") public URLClassLoader2(URL[] urls, ClassLoader parent) { super(urls, parent); } + /** + * Create a new {@link URLClassLoader2} with the given name and URLS and the {@link #getSystemClassLoader()} as its parent. + * @param name name of this classloader. + * @param urls the list of URLS to find classes in. + * @since 2.459 + */ + public URLClassLoader2(String name, URL[] urls) { + super(name, urls, getSystemClassLoader()); + } + + /** + * Create a new {@link URLClassLoader2} with the given name, URLS parent. + * @param name name of this classloader. + * @param urls the list of URLS to find classes in. + * @param parent the parent to search for classes before we look in the {@code urls} + * @since 2.459 + */ + public URLClassLoader2(String name, URL[] urls, ClassLoader parent) { + super(name, urls, parent); + } + @Override public void addURL(URL url) { super.addURL(url); diff --git a/core/src/main/java/jenkins/util/VirtualFile.java b/core/src/main/java/jenkins/util/VirtualFile.java index bf9fc49bd48d..6c754a50bbc4 100644 --- a/core/src/main/java/jenkins/util/VirtualFile.java +++ b/core/src/main/java/jenkins/util/VirtualFile.java @@ -46,6 +46,7 @@ import java.io.Serializable; import java.net.URI; import java.net.URL; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.OpenOption; @@ -375,7 +376,7 @@ public int zip(OutputStream outputStream, String includes, String excludes, bool Collection files = list(includes, excludes, useDefaultExcludes, openOptions); try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { - zos.setEncoding(System.getProperty("file.encoding")); // TODO JENKINS-20663 make this overridable via query parameter + zos.setEncoding(Charset.defaultCharset().displayName()); // TODO JENKINS-20663 make this overridable via query parameter for (String relativePath : files) { VirtualFile virtualFile = this.child(relativePath); diff --git a/core/src/main/java/jenkins/widgets/BuildTimeTrend.java b/core/src/main/java/jenkins/widgets/BuildTimeTrend.java index 59ab879f05da..dd0f0434ecb3 100644 --- a/core/src/main/java/jenkins/widgets/BuildTimeTrend.java +++ b/core/src/main/java/jenkins/widgets/BuildTimeTrend.java @@ -25,7 +25,9 @@ package jenkins.widgets; import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; import hudson.model.BallColor; +import hudson.model.Job; import hudson.model.Node; import hudson.model.Run; import jenkins.console.ConsoleUrlProvider; @@ -37,6 +39,10 @@ @Restricted(DoNotUse.class) // only for buildTimeTrend.jelly public class BuildTimeTrend extends RunListProgressiveRendering { + public boolean isAbstractProject(Job job) { + return job instanceof AbstractProject; + } + @Override protected void calculate(Run build, JSONObject element) { BallColor iconColor = build.getIconColor(); element.put("iconName", iconColor.getIconName()); @@ -46,6 +52,8 @@ public class BuildTimeTrend extends RunListProgressiveRendering { element.put("displayName", build.getDisplayName()); element.put("duration", build.getDuration()); element.put("durationString", build.getDurationString()); + element.put("timestampString", build.getTimestampString()); + element.put("timestampString2", build.getTimestampString2()); element.put("consoleUrl", ConsoleUrlProvider.getRedirectUrl(build)); if (build instanceof AbstractBuild) { AbstractBuild b = (AbstractBuild) build; diff --git a/core/src/main/java/jenkins/widgets/HistoryPageEntry.java b/core/src/main/java/jenkins/widgets/HistoryPageEntry.java index ac5e0a772692..98f0a7aac58e 100644 --- a/core/src/main/java/jenkins/widgets/HistoryPageEntry.java +++ b/core/src/main/java/jenkins/widgets/HistoryPageEntry.java @@ -57,8 +57,7 @@ public long getEntryId() { protected static long getEntryId(@NonNull Object entry) { if (entry instanceof QueueItem) { return ((QueueItem) entry).getId(); - } else if (entry instanceof Run) { - Run run = (Run) entry; + } else if (entry instanceof Run run) { return Long.MIN_VALUE + run.getNumber(); } else if (entry instanceof Number) { // Used for testing purposes because of JENKINS-30899 and JENKINS-30909 diff --git a/core/src/main/java/org/jenkins/ui/icon/Icon.java b/core/src/main/java/org/jenkins/ui/icon/Icon.java index e99822dac653..95524c8c4b74 100644 --- a/core/src/main/java/org/jenkins/ui/icon/Icon.java +++ b/core/src/main/java/org/jenkins/ui/icon/Icon.java @@ -279,7 +279,7 @@ public static String toNormalizedCSSSelector(String classNames) { // Trim all tokens first for (String classNameTok : classNameTokA) { String trimmedToken = classNameTok.trim(); - if (trimmedToken.length() > 0) { + if (!trimmedToken.isEmpty()) { classNameTokL.add(trimmedToken); } } diff --git a/core/src/main/java/org/jenkins/ui/symbol/Symbol.java b/core/src/main/java/org/jenkins/ui/symbol/Symbol.java index 9973acc18da5..52c7faee0249 100644 --- a/core/src/main/java/org/jenkins/ui/symbol/Symbol.java +++ b/core/src/main/java/org/jenkins/ui/symbol/Symbol.java @@ -8,13 +8,31 @@ import hudson.Util; import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; +import org.apache.tools.ant.filters.StringInputStream; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; /** * Helper class to load symbols from Jenkins core or plugins. @@ -55,13 +73,13 @@ public static String get(@NonNull SymbolRequest request) { .computeIfAbsent(identifier, key -> new ConcurrentHashMap<>()) .computeIfAbsent(name, key -> loadSymbol(identifier, key)); if ((tooltip != null && !tooltip.isBlank()) && (htmlTooltip == null || htmlTooltip.isBlank())) { - symbol = symbol.replaceAll(").*?()", "$1$2") - .replaceAll(" "The given src for the svg is not a valid xml document"); + return PLACEHOLDER_SVG; + } + + return markup; } @CheckForNull diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index a0b5fd3edb0f..99c77ebbed46 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -67,7 +67,7 @@ PluginManager.UnexpectedException=Unexpected exception going through the retryin PluginManager.compatWarning=\ - Warning: The new version of this plugin is marked as incompatible with the installed version. \ + Warning: The new version of this plugin is marked as incompatible with the installed version of the plugin. \ This is usually the case because its behavior or APIs changed, or because it uses a different settings format than the installed version. \ Other plugins with a dependency on this plugin may be incompatible with this update and no longer work as expected, jobs using this plugin may need to be reconfigured, and/or you may not be able to cleanly revert to the prior version without manually restoring old settings. \ Consult the plugin release notes for details. @@ -110,6 +110,7 @@ ProxyConfiguration.TestUrlRequired=Test URL is required. ProxyConfiguration.MalformedTestUrl=Malformed Test URL {0}. ProxyConfiguration.FailedToConnectViaProxy=Failed to connect to {0}. ProxyConfiguration.FailedToConnect=Failed to connect to {0} (code {1}). +ProxyConfiguration.NonTLSWarning=Jenkins only supports using an http connection to the proxy. The credentials may be exposed to someone on the same network. ProxyConfiguration.Success=Success (code {0}) Functions.NoExceptionDetails=No Exception details diff --git a/core/src/main/resources/hudson/Messages_es.properties b/core/src/main/resources/hudson/Messages_es.properties index d1ae229de9bc..298ef23821d8 100644 --- a/core/src/main/resources/hudson/Messages_es.properties +++ b/core/src/main/resources/hudson/Messages_es.properties @@ -99,6 +99,7 @@ ProxyConfiguration.TestUrlRequired=Se requiere un URL de prueba. ProxyConfiguration.MalformedTestUrl=La URL de prueba está mal formada. ProxyConfiguration.FailedToConnectViaProxy=No se puede conectar a {0}. ProxyConfiguration.FailedToConnect=No se puede conectar a {0} (código {1}). +ProxyConfiguration.NonTLSWarning=Jenkins solo soporta conexiones http con el proxy. Las credenciales podrían quedar expuestas a cualquiera que se encuentre en la misma red. ProxyConfiguration.Success=Configurado (código {0}) Functions.NoExceptionDetails=No hay detalles de la excepción diff --git a/core/src/main/resources/hudson/Messages_fr.properties b/core/src/main/resources/hudson/Messages_fr.properties index db45e76ab397..1e0286deddc5 100644 --- a/core/src/main/resources/hudson/Messages_fr.properties +++ b/core/src/main/resources/hudson/Messages_fr.properties @@ -109,6 +109,7 @@ ProxyConfiguration.TestUrlRequired=Une URL de test est requise. ProxyConfiguration.MalformedTestUrl=L''URL de test {0} n''est pas correctement formée. ProxyConfiguration.FailedToConnectViaProxy=Impossible de se connecter à {0}. ProxyConfiguration.FailedToConnect=Impossible de se connecter à {0} (code {1}). +ProxyConfiguration.NonTLSWarning=Jenkins ne prend en charge que l''utilisation d''une connexion http vers le proxy. Les informations d''identification peuvent être exposées à une personne qui se trouve sur le même réseau. ProxyConfiguration.Success=Succès (code {0}) Functions.NoExceptionDetails=Aucun détail concernant l''exception diff --git a/core/src/main/resources/hudson/Messages_it.properties b/core/src/main/resources/hudson/Messages_it.properties index b929e6447cf8..2747e2d39366 100644 --- a/core/src/main/resources/hudson/Messages_it.properties +++ b/core/src/main/resources/hudson/Messages_it.properties @@ -106,6 +106,7 @@ PluginWrapper.PluginWrapperAdministrativeMonitor.DisplayName=Errore \ ProxyConfiguration.FailedToConnect=Impossibile connettersi a {0} (codice {1}). ProxyConfiguration.FailedToConnectViaProxy=Impossibile connettersi a {0}. ProxyConfiguration.MalformedTestUrl=URL di prova {0} malformato. +ProxyConfiguration.NonTLSWarning=Jenkins supporta solo l''utilizzo di una connessione http al proxy. Le credenziali potrebbero essere esposte a qualcuno sulla stessa rete. ProxyConfiguration.Success=Connessione riuscita (codice {0}) ProxyConfiguration.TestUrlRequired=È richiesto un URL di prova. TcpSlaveAgentListener.PingAgentProtocol.displayName=Protocollo ping diff --git a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly index 5ccac430ecd2..cb20ebf8dfe7 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginCycleDependenciesMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -

+
${%PluginCycles}
diff --git a/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly index 45c4cead0a44..1f5ec400bd3c 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginDeprecationMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -
+
${%DeprecatedPlugins}
diff --git a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly index 73182ec483e4..b1f9ba193d49 100644 --- a/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly +++ b/core/src/main/resources/hudson/PluginManager/PluginUpdateMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -
+
${%RequiredPluginUpdates}
diff --git a/core/src/main/resources/hudson/PluginManager/advanced.jelly b/core/src/main/resources/hudson/PluginManager/advanced.jelly index 8d0e6abcd22e..81948afa6e80 100644 --- a/core/src/main/resources/hudson/PluginManager/advanced.jelly +++ b/core/src/main/resources/hudson/PluginManager/advanced.jelly @@ -37,7 +37,7 @@ THE SOFTWARE. -
+
${%proxyMovedBlurb(rootURL+"/manage/configure")}
diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 8013742cf738..3c3ee43e0193 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -54,7 +54,7 @@ THE SOFTWARE. data-is-restart-required="${app.updateCenter.isRestartRequiredForCompletion()}" /> -
${%Warning}: ${%requires.restart}
+
${%Warning}: ${%requires.restart}

${%Build Time Trend}

-
+
+
+ + ${handler.setBuilds(it.builds)} + + + + + + + + + + + + + + + + + +
${%S}${%Build}${%Time Since}${%Duration}${%Agent}
+ +
[${%Build time graph}]
- - -
- - ${handler.setBuilds(it.builds)} - - - - - - - - - - -
${%Build}${%Duration}${%Agent}
-
diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css index 13efe1c1e375..af8ba3705509 100644 --- a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css +++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.css @@ -1,3 +1,10 @@ -img.build-time-graph { - float: right; +#buildTimeTrend { + display: flex; + gap: 15px; +} + +@media (max-width: 1300px) { + #buildTimeTrend { + flex-direction: column-reverse; + } } diff --git a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js index fd59ff650677..9ea25a3441de 100644 --- a/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js +++ b/core/src/main/resources/hudson/model/Job/buildTimeTrend_resources.js @@ -2,30 +2,31 @@ * Public method to be called by progressiveRendering's callback */ window.buildTimeTrend_displayBuilds = function (data) { - var p = document.getElementById("trend"); - var isDistributedBuildsEnabled = - "true" === p.getAttribute("data-is-distributed-build-enabled"); - var rootURL = document.head.getAttribute("data-rooturl"); + const p = document.getElementById("trend"); + p.classList.remove("jenkins-hidden"); - for (var x = 0; data.length > x; x++) { - var e = data[x]; - var tr = document.createElement("tr"); + const showAgent = "true" === p.dataset.showAgent; + const rootURL = document.head.getAttribute("data-rooturl"); + + for (let x = 0; data.length > x; x++) { + const e = data[x]; + let tr = document.createElement("tr"); let td = document.createElement("td"); td.setAttribute("data", e.iconColorOrdinal); - - let link = document.createElement("a"); - link.classList.add("build-status-link"); - link.href = e.consoleUrl; - td.appendChild(link); + td.classList.add("jenkins-table__cell--tight", "jenkins-table__icon"); + let div = document.createElement("div"); + div.classList.add("jenkins-table__cell__button-wrapper"); let svg = generateSVGIcon(e.iconName); - link.appendChild(svg); + svg.setAttribute("tooltip", e.iconColorDescription); + div.appendChild(svg); + td.appendChild(div); tr.appendChild(td); td = document.createElement("td"); td.setAttribute("data", e.number); - link = document.createElement("a"); + let link = document.createElement("a"); link.href = e.number + "/"; link.classList.add("model-link", "inside"); link.innerText = escapeHTML(e.displayName); @@ -33,15 +34,20 @@ window.buildTimeTrend_displayBuilds = function (data) { td.appendChild(link); tr.appendChild(td); + td = document.createElement("td"); + td.setAttribute("data", e.timestampString2); + td.textContent = e.timestampString; + tr.appendChild(td); + td = document.createElement("td"); td.setAttribute("data", e.duration); td.innerText = escapeHTML(e.durationString); tr.appendChild(td); - if (isDistributedBuildsEnabled) { - var buildInfo = null; - var buildInfoStr = escapeHTML(e.builtOnStr || ""); + if (showAgent) { + let buildInfo = null; + let buildInfoStr = escapeHTML(e.builtOnStr || ""); if (e.builtOn) { buildInfo = document.createElement("a"); buildInfo.href = rootURL + "/computer/" + e.builtOn; @@ -58,6 +64,19 @@ window.buildTimeTrend_displayBuilds = function (data) { } tr.appendChild(td); } + + let tdConsole = document.createElement("td"); + tdConsole.classList.add("jenkins-table__cell--tight"); + let div2 = document.createElement("div"); + div2.classList.add("jenkins-table__cell__button-wrapper"); + link = document.createElement("a"); + link.classList.add("jenkins-button", "jenkins-button--tertiary"); + link.href = e.consoleUrl; + link.appendChild(generateSVGIcon("console")); + div2.appendChild(link); + tdConsole.appendChild(div2); + tr.appendChild(tdConsole); + p.appendChild(tr); Behaviour.applySubtree(tr); } @@ -132,7 +151,7 @@ window.displayBuilds = function (data) { var div2 = document.createElement("div"); div2.classList.add("jenkins-table__cell__button-wrapper"); var a3 = document.createElement("a"); - a3.classList.add("jenkins-table__button"); + a3.classList.add("jenkins-button", "jenkins-button--tertiary"); a3.href = e.consoleUrl; a3.innerHTML = p.dataset.consoleOutputIcon; div2.appendChild(a3); diff --git a/core/src/main/resources/hudson/model/Job/index.jelly b/core/src/main/resources/hudson/model/Job/index.jelly index 0fb537e267d7..d9b214900c7b 100644 --- a/core/src/main/resources/hudson/model/Job/index.jelly +++ b/core/src/main/resources/hudson/model/Job/index.jelly @@ -40,6 +40,9 @@ THE SOFTWARE.
+
+ +
@@ -52,7 +55,7 @@ THE SOFTWARE. - + diff --git a/core/src/main/resources/hudson/model/Label/index.jelly b/core/src/main/resources/hudson/model/Label/index.jelly index 34ede2f6c8ee..ad75b1dbf4c7 100644 --- a/core/src/main/resources/hudson/model/Label/index.jelly +++ b/core/src/main/resources/hudson/model/Label/index.jelly @@ -28,12 +28,13 @@ THE SOFTWARE. -

- ${it.name} -

- + + + + + - + diff --git a/core/src/main/resources/hudson/model/Messages.properties b/core/src/main/resources/hudson/model/Messages.properties index 96950428fee4..d56c57ff5ee0 100644 --- a/core/src/main/resources/hudson/model/Messages.properties +++ b/core/src/main/resources/hudson/model/Messages.properties @@ -241,7 +241,7 @@ Run.UpdatePermission.Description=\ Run.ArtifactsPermission.Description=\ This permission grants the ability to retrieve the artifacts produced by \ builds. If you don’t want an user to access the artifacts, you can do so by \ - revoking this permission. + revoking this permission. This does not restrict the listing of artifacts. Run.InProgressDuration={0} and counting Run.NotStartedYet=Not started yet Run.ArtifactsBrowserTitle=Artifacts of {0} {1} @@ -279,7 +279,7 @@ View.DeletePermission.Description=\ View.ConfigurePermission.Description=\ This permission allows users to change the configuration of views. View.ReadPermission.Description=\ - This permission allows users to see views (implied by generic read access). + This permission allows users to see views. View.MissingMode=No view type is specified View.DisplayNameNotUniqueWarning=The display name, "{0}", is already in use by another view and \ could cause confusion and delay. @@ -422,3 +422,5 @@ ManagementLink.Category.MISC=Other ManagementLink.Category.UNCATEGORIZED=Uncategorized FileParameterValue.IndexTitle=File Parameters + +UserPreferencesProperty.DisplayName=Preferences diff --git a/core/src/main/resources/hudson/model/Messages_fr.properties b/core/src/main/resources/hudson/model/Messages_fr.properties index 1d64aee184f3..e6794e6fc49e 100644 --- a/core/src/main/resources/hudson/model/Messages_fr.properties +++ b/core/src/main/resources/hudson/model/Messages_fr.properties @@ -231,7 +231,8 @@ Run.UpdatePermission.Description=\ par exemple pour laisser des notes sur la cause d''échec d''un build. Run.ArtifactsPermission.Description=\ Ce droit permet de récupérer les artefacts produits par les builds. \ - Si un utilisateur ne doit pas accéder aux artefacts, vous pouvez l''en empêcher en désactivant ce droit. + Si un utilisateur ne doit pas accéder aux artefacts, vous pouvez l''en empêcher en désactivant ce droit. \ + Ce droit ne limite pas la possibilité de lister les artefacts. Run.InProgressDuration={0} et décompte Run.NotStartedYet=Pas encore commencé Run.ArtifactsBrowserTitle=Artefacts de {0} {1} diff --git a/core/src/main/resources/hudson/model/Run/KeepLogBuildBadge/badge.jelly b/core/src/main/resources/hudson/model/Run/KeepLogBuildBadge/badge.jelly index 0703936eced5..4c60feb56650 100644 --- a/core/src/main/resources/hudson/model/Run/KeepLogBuildBadge/badge.jelly +++ b/core/src/main/resources/hudson/model/Run/KeepLogBuildBadge/badge.jelly @@ -23,4 +23,4 @@ THE SOFTWARE. --> - + diff --git a/core/src/main/resources/hudson/model/Run/console.jelly b/core/src/main/resources/hudson/model/Run/console.jelly index 519b61744a27..ad38828dad5a 100644 --- a/core/src/main/resources/hudson/model/Run/console.jelly +++ b/core/src/main/resources/hudson/model/Run/console.jelly @@ -28,15 +28,28 @@ THE SOFTWARE. Displays the console output --> - + - + + + - + + + + ${%Download} + + + + ${%View as plain text} + + + + ${%Console Output} - + @@ -71,4 +84,4 @@ THE SOFTWARE. - + diff --git a/core/src/main/resources/hudson/model/Run/console.properties b/core/src/main/resources/hudson/model/Run/console.properties index d4467f9be774..c9af02ab9aa3 100644 --- a/core/src/main/resources/hudson/model/Run/console.properties +++ b/core/src/main/resources/hudson/model/Run/console.properties @@ -21,5 +21,3 @@ # THE SOFTWARE. skipSome=Skipping {0,number,integer} KB.. Full Log -clickToCopy=Click to copy -successfullyCopied=Copied to clipboard \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Run/consoleFull.jelly b/core/src/main/resources/hudson/model/Run/consoleFull.jelly index 6e4a820d9d33..feed99c66479 100644 --- a/core/src/main/resources/hudson/model/Run/consoleFull.jelly +++ b/core/src/main/resources/hudson/model/Run/consoleFull.jelly @@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + - + diff --git a/core/src/main/resources/hudson/model/Run/index.jelly b/core/src/main/resources/hudson/model/Run/index.jelly index 0acdb74fb01f..b164817a164f 100644 --- a/core/src/main/resources/hudson/model/Run/index.jelly +++ b/core/src/main/resources/hudson/model/Run/index.jelly @@ -30,10 +30,20 @@ THE SOFTWARE. -
+ + + + + ${it.displayName} () + +
+ +
+ +
${%startedAgo(it.timestampString)}
@@ -48,11 +58,6 @@ THE SOFTWARE.
- ${it.displayName} () - -
- -
-
+ diff --git a/core/src/main/resources/hudson/model/Run/statusIcon.jelly b/core/src/main/resources/hudson/model/Run/statusIcon.jelly index e481fd48761d..548d5176bcea 100644 --- a/core/src/main/resources/hudson/model/Run/statusIcon.jelly +++ b/core/src/main/resources/hudson/model/Run/statusIcon.jelly @@ -28,7 +28,7 @@ THE SOFTWARE. Displays the console output --> - + @@ -37,4 +37,4 @@ THE SOFTWARE. - +
diff --git a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message.jelly b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message.jelly index 8419f13ce05c..f11ee8837cdd 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message.jelly +++ b/core/src/main/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message.jelly @@ -24,7 +24,7 @@ THE SOFTWARE. -
+
diff --git a/core/src/main/resources/hudson/model/UpdateCenter/_download-failure.js b/core/src/main/resources/hudson/model/UpdateCenter/_download-failure.js index 2040aab6684e..7d4bf51ff843 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/_download-failure.js +++ b/core/src/main/resources/hudson/model/UpdateCenter/_download-failure.js @@ -5,7 +5,7 @@ Behaviour.specify( function (anchor) { anchor.onclick = function (event) { event.preventDefault(); - // eslint-disable-next-line + // eslint-disable-next-line no-undef const n = findNext(this, function (el) { return el.tagName === "PRE"; }); diff --git a/core/src/main/resources/hudson/model/User/configure.jelly b/core/src/main/resources/hudson/model/User/configure.jelly deleted file mode 100644 index 9b4100248559..000000000000 --- a/core/src/main/resources/hudson/model/User/configure.jelly +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/src/main/resources/hudson/model/User/index.jelly b/core/src/main/resources/hudson/model/User/index.jelly index db5a8f83022e..a85c46b1a945 100644 --- a/core/src/main/resources/hudson/model/User/index.jelly +++ b/core/src/main/resources/hudson/model/User/index.jelly @@ -27,13 +27,21 @@ THE SOFTWARE. -

- - - - ${it.fullName} -

- +
+
+

+ + + + ${it.fullName} +

+
+
+ +
+
+ +
${%Jenkins User ID}: ${it.id}
diff --git a/core/src/main/resources/hudson/model/User/sidepanel.jelly b/core/src/main/resources/hudson/model/User/sidepanel.jelly index 834061995daf..f8024b0c21b8 100644 --- a/core/src/main/resources/hudson/model/User/sidepanel.jelly +++ b/core/src/main/resources/hudson/model/User/sidepanel.jelly @@ -33,7 +33,6 @@ THE SOFTWARE. - diff --git a/core/src/main/resources/hudson/model/View/index.jelly b/core/src/main/resources/hudson/model/View/index.jelly index 3f20913e81cc..14632b8e60e8 100644 --- a/core/src/main/resources/hudson/model/View/index.jelly +++ b/core/src/main/resources/hudson/model/View/index.jelly @@ -23,7 +23,7 @@ THE SOFTWARE. --> - + @@ -44,9 +44,8 @@ THE SOFTWARE.
- + diff --git a/core/src/test/java/hudson/GetLocaleStaticHelpUrlTest.java b/core/src/test/java/hudson/GetLocaleStaticHelpUrlTest.java index 5fd822bc1c55..a61fc0d54656 100644 --- a/core/src/test/java/hudson/GetLocaleStaticHelpUrlTest.java +++ b/core/src/test/java/hudson/GetLocaleStaticHelpUrlTest.java @@ -36,6 +36,7 @@ import java.util.Enumeration; import java.util.Locale; import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.Issue; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.lang.Klass; @@ -191,6 +192,24 @@ public void getStaticHelpUrlAcceptZhResMore() { assertThatLocaleResourceIs(id, "help-id_zh_CN.html"); } + @Issue("JENKINS-73246") + @Test + public void getStaticHelpUrlAcceptEnFirst() { + // Accept-Language: en-US,en;q=0.9,de;q=0.8 + StaplerRequest req = mockStaplerRequest( + Locale.US, + Locale.ENGLISH, + Locale.GERMAN + ); + + Klass klass = mockKlass( + "help-id.html", + "help-id_de.html" + ); + URL id = Descriptor.getStaticHelpUrl(req, klass, "-id"); + assertThatLocaleResourceIs(id, "help-id.html"); + } + private StaplerRequest mockStaplerRequest(Locale... localeArr) { StaplerRequest req = mock(StaplerRequest.class); Enumeration locales = Collections.enumeration(Arrays.asList(localeArr)); diff --git a/core/src/test/java/hudson/PluginWrapperTest.java b/core/src/test/java/hudson/PluginWrapperTest.java index 32c24d8abf36..c33877a3b6fe 100644 --- a/core/src/test/java/hudson/PluginWrapperTest.java +++ b/core/src/test/java/hudson/PluginWrapperTest.java @@ -105,7 +105,7 @@ public void dependencyFailedToLoad() { @Issue("JENKINS-66563") @Test public void insertJarsIntoClassPath() throws Exception { - try (URLClassLoader2 cl = new URLClassLoader2(new URL[0])) { + try (URLClassLoader2 cl = new URLClassLoader2("Test", new URL[0])) { assertInjectingJarsWorks(cl); } } diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 3fd34ff5c2ad..ac08dccb5d22 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -248,7 +248,7 @@ public void testSymlink() throws Exception { Util.createSymlink(d, buf.toString(), "x", l); String log = baos.toString(Charset.defaultCharset()); - if (log.length() > 0) + if (!log.isEmpty()) System.err.println("log output: " + log); assertEquals(buf.toString(), Util.resolveSymlink(new File(d, "x"))); diff --git a/core/src/test/java/hudson/cli/ListJobsCommandTest.java b/core/src/test/java/hudson/cli/ListJobsCommandTest.java index 6be35fded558..8baa6814b95b 100644 --- a/core/src/test/java/hudson/cli/ListJobsCommandTest.java +++ b/core/src/test/java/hudson/cli/ListJobsCommandTest.java @@ -191,7 +191,7 @@ protected boolean matchesSafely(ByteArrayOutputStream item) { } catch (InterruptedException e) { throw new RuntimeException(e); } - Set jobs = new HashSet<>(Arrays.asList(item.toString(charset).split(System.getProperty("line.separator")))); + Set jobs = new HashSet<>(Arrays.asList(item.toString(charset).split(System.lineSeparator()))); return new HashSet<>(Arrays.asList(expected)).equals(jobs); } diff --git a/core/src/test/java/hudson/console/LineTransformationOutputStreamTest.java b/core/src/test/java/hudson/console/LineTransformationOutputStreamTest.java new file mode 100644 index 000000000000..d9fefc29123d --- /dev/null +++ b/core/src/test/java/hudson/console/LineTransformationOutputStreamTest.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright 2024 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.console; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Test; + +public final class LineTransformationOutputStreamTest { + + @Test public void nl() throws Exception { + test("\n"); + } + + @Test public void crnl() throws Exception { + test("\r\n"); + } + + @Test public void cr() throws Exception { + test("\r"); + } + + private void test(String linefeed) throws Exception { + var count = new AtomicLong(); + long max = 1_000_000; // to see OOME in cr without fix: 1_000_000_000 + try (var counter = new LineTransformationOutputStream() { + @Override protected void eol(byte[] b, int len) throws IOException { + var line = new String(b, 0, len); + assertThat(line, endsWith(linefeed)); + count.addAndGet(Integer.parseInt(trimEOL(line))); + } + }) { + for (long i = 0; i < max; i++) { + counter.write((i + linefeed).getBytes(StandardCharsets.UTF_8)); + } + } + assertThat(count.get(), is((max * (max - 1)) / 2)); + } + +} diff --git a/core/src/test/java/hudson/model/RunTest.java b/core/src/test/java/hudson/model/RunTest.java index 59b32ab915c5..04448da1f12a 100644 --- a/core/src/test/java/hudson/model/RunTest.java +++ b/core/src/test/java/hudson/model/RunTest.java @@ -197,7 +197,7 @@ public void getLogReturnsAnRightOrder() throws Exception { for (int i = 1; i < 10; i++) { assertEquals("dummy" + (10 + i), logLines.get(i)); } - int truncatedCount = 10 * ("dummyN".length() + System.getProperty("line.separator").length()) - 2; + int truncatedCount = 10 * ("dummyN".length() + System.lineSeparator().length()) - 2; assertEquals("[...truncated " + truncatedCount + " B...]", logLines.get(0)); } diff --git a/core/src/test/java/hudson/util/RetrierTest.java b/core/src/test/java/hudson/util/RetrierTest.java index 50abb7ded2b1..42242b44f4c7 100644 --- a/core/src/test/java/hudson/util/RetrierTest.java +++ b/core/src/test/java/hudson/util/RetrierTest.java @@ -160,7 +160,7 @@ public void failedActionWithExceptionAfterThreeAttemptsWithoutListenerTest() thr throw new IndexOutOfBoundsException("Exception allowed considered as failure"); }, // check the result and return true (boolean primitive type) if success - (currentAttempt, result) -> result == null ? false : result, + (currentAttempt, result) -> result != null && result, //name of the action ACTION ) diff --git a/core/src/test/java/hudson/util/SecretRewriterTest.java b/core/src/test/java/hudson/util/SecretRewriterTest.java deleted file mode 100644 index ff13cd915254..000000000000 --- a/core/src/test/java/hudson/util/SecretRewriterTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package hudson.util; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeFalse; - -import hudson.FilePath; -import hudson.Functions; -import hudson.model.TaskListener; -import java.io.File; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; -import java.util.List; -import java.util.regex.Pattern; -import javax.crypto.Cipher; -import jenkins.security.ConfidentialStoreRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -public class SecretRewriterTest { - - @Rule - public ConfidentialStoreRule confidentialStoreRule = new ConfidentialStoreRule(); - - @Rule - public TemporaryFolder tmp = new TemporaryFolder(); - - private static final Pattern FOO_PATTERN = Pattern.compile("[{][A-Za-z0-9+/]+={0,2}[}]"); - private static final Pattern MSG_PATTERN = Pattern.compile("[{][A-Za-z0-9+/]+={0,2}[}]"); - private static final Pattern FOO_PATTERN2 = Pattern.compile("([{][A-Za-z0-9+/]+={0,2}[}]){2}"); - private static final Pattern ABC_FOO_PATTERN = Pattern.compile("\\s[{][A-Za-z0-9+/]+={0,2}[}]\\s"); - - @Test - public void singleFileRewrite() throws Exception { - String o = encryptOld("foobar"); // old - String n = encryptNew("foobar"); // new - assertTrue(FOO_PATTERN.matcher(roundtrip("" + o + "")).matches()); - assertTrue(FOO_PATTERN2.matcher(roundtrip("" + o + "" + o + "")).matches()); - assertEquals("" + n + "", roundtrip("" + n + "")); - assertEquals("thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret", roundtrip(" thisIsLegalBase64AndLongEnoughThatItCouldLookLikeSecret ")); - // to be rewritten, it needs to be between a tag - assertEquals("" + o, roundtrip("" + o)); - assertEquals(o + "", roundtrip(o + "")); - assertTrue(ABC_FOO_PATTERN.matcher(roundtrip("\n" + o + "\n")).matches()); - } - - private String roundtrip(String before) throws Exception { - SecretRewriter sr = new SecretRewriter(); - Path path = Files.createTempFile(tmp.getRoot().toPath(), "test", "xml"); - Files.writeString(path, before, Charset.defaultCharset()); - sr.rewrite(path.toFile()); - //assert after.replaceAll(System.getProperty("line.separator"), "\n").trim()==f.text.replaceAll(System.getProperty("line.separator"), "\n").trim() - return Files.readString(path, Charset.defaultCharset()).replaceAll(System.getProperty("line.separator"), "\n").trim(); - } - - @SuppressWarnings("deprecation") - private String encryptOld(String str) throws Exception { - Cipher cipher = Secret.getCipher("AES"); - cipher.init(Cipher.ENCRYPT_MODE, HistoricalSecrets.getLegacyKey()); - return Base64.getEncoder().encodeToString(cipher.doFinal((str + HistoricalSecrets.MAGIC).getBytes(StandardCharsets.UTF_8))); - } - - private String encryptNew(String str) { - return Secret.fromString(str).getEncryptedValue(); - } - - /** - * Directory rewrite and recursion detection - */ - @Test - public void recursionDetection() throws Exception { - assumeFalse("Symlinks don't work on Windows very well", Functions.isWindows()); - SecretRewriter sw = new SecretRewriter(); - TaskListener st = StreamTaskListener.fromStdout(); - - String o = encryptOld("Hello world"); - String n = encryptNew("Hello world"); - String payload = "" + o + ""; - - // set up some directories with stuff - File t = tmp.newFolder("t"); - List dirs = List.of("a", "b", "c", "c/d", "c/d/e"); - for (String p : dirs) { - File d = new File(t, p); - d.mkdir(); - Files.writeString(d.toPath().resolve("foo.xml"), payload, Charset.defaultCharset()); - } - - // stuff outside - File t2 = tmp.newFolder("t2"); - Files.writeString(t2.toPath().resolve("foo.xml"), payload, Charset.defaultCharset()); - - // some recursions as well as valid symlinks - new FilePath(t).child("c/symlink").symlinkTo("..", st); - new FilePath(t).child("b/symlink").symlinkTo(".", st); - new FilePath(t).child("a/symlink").symlinkTo(t2.getAbsolutePath(), st); - - assertEquals(6, sw.rewriteRecursive(t, st)); - - for (String p : dirs) { - assertTrue(MSG_PATTERN.matcher(Files.readString(new File(t, p + "/foo.xml").toPath(), Charset.defaultCharset()).trim()).matches()); - } - - // t2 is only reachable by following a symlink. this should be covered, too - assertTrue(MSG_PATTERN.matcher(Files.readString(new File(t2, "foo.xml").toPath(), Charset.defaultCharset()).trim()).matches()); - } - -} diff --git a/core/src/test/java/hudson/util/jna/GNUCLibraryTest.java b/core/src/test/java/hudson/util/jna/GNUCLibraryTest.java index c0987d7d4e8e..d3b7bea3864a 100644 --- a/core/src/test/java/hudson/util/jna/GNUCLibraryTest.java +++ b/core/src/test/java/hudson/util/jna/GNUCLibraryTest.java @@ -6,7 +6,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; -import com.sun.jna.Native; import hudson.Functions; import java.io.IOException; import java.nio.file.Files; @@ -15,7 +14,6 @@ public class GNUCLibraryTest { - private static final int EBADF = 9; private static final int O_CREAT = "Linux".equals(System.getProperty("os.name")) ? 64 : 512; private static final int O_RDWR = 2; @@ -29,10 +27,6 @@ public void openTest() throws IOException { int result = GNUCLibrary.LIBC.close(fd); assertEquals(0, result); - result = GNUCLibrary.LIBC.close(fd); - assertEquals(-1, result); - assertEquals(EBADF, Native.getLastError()); - Path tmpFile = Files.createTempFile("openTest", null); Files.delete(tmpFile); assertFalse(Files.exists(tmpFile)); @@ -56,10 +50,6 @@ public void closeTest() { int result = GNUCLibrary.LIBC.close(fd); assertEquals(0, result); - - result = GNUCLibrary.LIBC.close(fd); - assertEquals(-1, result); - assertEquals(EBADF, Native.getLastError()); } @Test diff --git a/core/src/test/java/jenkins/model/PeepholePermalinkTest.java b/core/src/test/java/jenkins/model/PeepholePermalinkTest.java index 8a2cb059b230..50a3934700cf 100644 --- a/core/src/test/java/jenkins/model/PeepholePermalinkTest.java +++ b/core/src/test/java/jenkins/model/PeepholePermalinkTest.java @@ -39,7 +39,7 @@ public void classLoadingDeadlock() throws Exception { PeepholePermalink.initialized(); Thread t = new Thread(() -> { assertThat("successfully loaded permalinks", - PermalinkProjectAction.Permalink.BUILTIN.stream().map(p -> p.getId()).collect(Collectors.toSet()), + PermalinkProjectAction.Permalink.BUILTIN.stream().map(PermalinkProjectAction.Permalink::getId).collect(Collectors.toSet()), containsInAnyOrder("lastBuild", "lastStableBuild", "lastSuccessfulBuild", "lastFailedBuild", "lastUnstableBuild", "lastUnsuccessfulBuild", "lastCompletedBuild")); }); t.start(); diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 000000000000..c25e303ba5db --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,104 @@ +const eslintConfigPrettier = require("eslint-config-prettier"); +const globals = require("globals"); +const js = require("@eslint/js"); + +module.exports = [ + // Global ignores + { + ignores: [ + "**/target/", + "**/work/", + + // Node + "**/node/", + + // Generated JavaScript Bundles + "**/jsbundles/", + + // External scripts + "war/.pnp.cjs", + "war/.pnp.loader.mjs", + "war/src/main/js/plugin-setup-wizard/bootstrap-detached.js", + "war/src/main/webapp/scripts/yui/*", + ], + }, + { + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.browser, + applyTooltip: "readonly", + AutoScroller: "readonly", + Behaviour: "readonly", + breadcrumbs: "readonly", + buildFormTree: "readonly", + CodeMirror: "readonly", + ComboBox: "readonly", + COMBOBOX_VERSION: "writeable", + createSearchBox: "readonly", + crumb: "readonly", + dialog: "readonly", + ensureVisible: "readonly", + escapeHTML: "readonly", + findAncestor: "readonly", + findAncestorClass: "readonly", + findElementsBySelector: "readonly", + findFormParent: "readonly", + fireEvent: "readonly", + Form: "readonly", + FormChecker: "readonly", + getElementOverflowParams: "readonly", + hoverNotification: "readonly", + iota: "writeable", + isInsideRemovable: "readonly", + isPageVisible: "readonly", + isRunAsTest: "readonly", + layoutUpdateCallback: "readonly", + loadScript: "readonly", + makeButton: "readonly", + notificationBar: "readonly", + object: "readonly", + objectToUrlFormEncoded: "readonly", + onSetupWizardInitialized: "readonly", + refillOnChange: "readonly", + refreshPart: "readonly", + registerSortableDragDrop: "readonly", + renderOnDemand: "readonly", + rootURL: "readonly", + safeValidateButton: "readonly", + setupWizardExtensions: "readonly", + SharedArrayBuffer: "readonly", + shortenName: "readonly", + Sortable: "readonly", + toQueryString: "readonly", + ts_refresh: "readonly", + updateOptionalBlock: "readonly", + Utilities: "readonly", + UTILITIES_VERSION: "writeable", + YAHOO: "readonly", + }, + }, + }, + // Uses eslint default ruleset + js.configs.recommended, + eslintConfigPrettier, + { + rules: { + curly: "error", + }, + }, + { + files: [ + "eslint.config.cjs", + "war/postcss.config.js", + "war/webpack.config.js", + "war/.stylelintrc.js", + ], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, +]; diff --git a/pom.xml b/pom.xml index 2015da5873e5..76fe0de99616 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ THE SOFTWARE. org.jenkins-ci jenkins - 1.113 + 1.119 @@ -53,6 +53,7 @@ THE SOFTWARE. bom websocket/spi websocket/jetty10 + websocket/jetty12-ee8 core war test @@ -73,8 +74,9 @@ THE SOFTWARE. - 2.454 + 2.474 -SNAPSHOT + 2024-08-20T13:40:21Z github @@ -86,19 +88,17 @@ THE SOFTWARE. https://www.jenkins.io/changelog - 3206.vb_15dcf73f6a_9 - - 4.13 + 3261.v9c670a_4748a_9 Max Medium 1.33 - 4.13.1 + 4.13.2 1.29 false - 6.18 + 7.0 + 3107.v665000b_51092 io.jenkins.plugins commons-text-api - 1.11.0-95.v22a_d30ee5d36 + 1.12.0-129.v99a_50df237f7 io.jenkins.plugins font-awesome-api - 6.5.1-3 + 6.6.0-1 io.jenkins.plugins ionicons-api - 70.v2959a_b_74e3cf + 74.v93d5eb_813d5f io.jenkins.plugins javax-activation-api - 1.2.0-6 + 1.2.0-7 io.jenkins.plugins @@ -93,41 +95,46 @@ THE SOFTWARE. org.jenkins-ci.plugins ant - 497.v94e7d9fffa_b_9 + 511.v0a_a_1a_334f41b_ + + + org.jenkins-ci.plugins + bouncycastle-api + 2.30.1.78.1-248.ve27176eb_46cb_ org.jenkins-ci.plugins display-url-api - 2.200.vb_9327d658781 + 2.204.vf6fddd8a_8b_e9 org.jenkins-ci.plugins scm-api - 689.v237b_6d3a_ef7f + 696.v778d637b_a_762 org.jenkins-ci.plugins script-security - 1335.vf07d9ce377a_e + 1354.va_70a_fe478c7f org.jenkins-ci.plugins.workflow workflow-api - 1291.v51fd2a_625da_7 + 1336.vee415d95c521 org.jenkins-ci.plugins.workflow workflow-step-api - 657.v03b_e8115821b_ + 678.v3ee58b_469476 org.jenkins-ci.plugins.workflow workflow-support - 896.v175a_a_9c5b_78f + 920.v59f71ce16f04 @@ -139,34 +146,6 @@ THE SOFTWARE. ${project.version} provided - - ${project.groupId} - jenkins-test-harness - 2186.v5a_b_e6dd02a_35 - test - - - ${project.groupId} - jenkins-war - - - org.hamcrest - hamcrest-core - - - - - ${project.groupId} - jenkins-test-harness-tools - 2.2 - test - - - ${project.groupId} - jenkins-test-harness - - - ${project.groupId} jenkins-war @@ -177,13 +156,13 @@ THE SOFTWARE. io.jenkins.plugins javax-mail-api - 1.6.2-9 + 1.6.2-10 test org.awaitility awaitility - 4.2.1 + 4.2.2 test @@ -196,6 +175,34 @@ THE SOFTWARE. test-annotations test + + org.jenkins-ci.main + jenkins-test-harness + 2254.vcff7a_d4969e5 + test + + + ${project.groupId} + jenkins-war + + + org.hamcrest + hamcrest-core + + + + + org.jenkins-ci.main + jenkins-test-harness-tools + 2.2 + test + + + org.jenkins-ci.main + jenkins-test-harness + + + org.jenkins-ci.modules instance-identity @@ -211,19 +218,19 @@ THE SOFTWARE. org.jenkins-ci.plugins cloudbees-folder - 6.928.v7c780211d66e + 6.942.vb_43318a_156b_2 test org.jenkins-ci.plugins credentials - 1337.v60b_d7b_c7b_c9f + 1371.vfee6b_095f0a_3 test org.jenkins-ci.plugins junit - 1265.v65b_14fa_f12f0 + 1296.vb_f538b_c88630 test @@ -241,13 +248,13 @@ THE SOFTWARE. org.jenkins-ci.plugins matrix-project - 822.824.v14451b_c0fd42 + 832.va_66e270d2946 test org.jenkins-ci.plugins structs - 337.v1b_04ea_4df7c8 + 338.v848422169819 test @@ -321,7 +328,15 @@ THE SOFTWARE. org.jenkins-ci.main remoting - 4.12 + 3256.v88a_f6e922152 + jar + ${project.build.outputDirectory}/old-remoting + remoting-before-SECURITY-3430-fix.jar + + + org.jenkins-ci.main + remoting + 3085.vc4c6977c075a jar ${project.build.outputDirectory}/old-remoting remoting-unsupported.jar @@ -329,7 +344,7 @@ THE SOFTWARE. io.jenkins.plugins design-library - 286.v6643a_81e02b_8 + 307.vecc0205ca_707 hpi ${project.build.outputDirectory}/plugins design-library.jpi @@ -337,7 +352,7 @@ THE SOFTWARE. io.jenkins.plugins prism-api - 1.29.0-13 + 1.29.0-17 hpi ${project.build.outputDirectory}/plugins prism-api.jpi @@ -470,5 +485,11 @@ THE SOFTWARE. + + release + + 1 + + diff --git a/test/src/test/java/hudson/PluginManagerTest.java b/test/src/test/java/hudson/PluginManagerTest.java index c2fc5dd8721e..91a236db0266 100644 --- a/test/src/test/java/hudson/PluginManagerTest.java +++ b/test/src/test/java/hudson/PluginManagerTest.java @@ -29,6 +29,7 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInRelativeOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; @@ -81,6 +82,7 @@ import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.servlet.ServletException; import jenkins.ClassLoaderReflectionToolkit; import jenkins.RestartRequiredException; @@ -576,8 +578,7 @@ private void dynamicLoadAndDisable(String plugin) throws IOException, Interrupte Thread.sleep(100); done = true; for (UpdateCenterJob job : r.jenkins.getUpdateCenter().getJobs()) { - if (job instanceof UpdateCenter.DownloadJob) { - UpdateCenter.DownloadJob j = (UpdateCenter.DownloadJob) job; + if (job instanceof UpdateCenter.DownloadJob j) { assertFalse(j.status instanceof UpdateCenter.DownloadJob.Failure); done &= !(j.status instanceof UpdateCenter.DownloadJob.Pending || j.status instanceof UpdateCenter.DownloadJob.Installing); @@ -626,6 +627,17 @@ public void optionalExtensionCanBeFoundAfterDynamicLoadOfVariant() throws Except assertTrue(ExtensionList.lookup(GlobalConfiguration.class).stream().anyMatch(gc -> "io.jenkins.plugins.MyGlobalConfiguration".equals(gc.getClass().getName()))); } + @Test @Issue("JENKINS-64840") + @WithPlugin({"mandatory-depender-0.0.2.hpi", "dependee-0.0.2.hpi", "depender-0.0.2.hpi"}) + public void getPluginsSortedByTitle() throws Exception { + List installedPlugins = r.jenkins.getPluginManager().getPluginsSortedByTitle() + .stream() + .map(PluginWrapper::getDisplayName) + .collect(Collectors.toUnmodifiableList()); + + assertThat(installedPlugins, containsInRelativeOrder("dependee", "depender", "mandatory-depender")); + } + @Issue("JENKINS-62622") @Test @WithPlugin("legacy.hpi") @@ -793,7 +805,7 @@ public void noInjectionOnAvailablePluginsPage() throws Exception { PluginManagerUtil.getCheckForUpdatesButton(p).click(); HtmlPage available = wc.goTo("pluginManager/available"); - assertTrue(available.querySelector(".alert-danger") + assertTrue(available.querySelector(".jenkins-alert-danger") .getTextContent().contains("This plugin is built for Jenkins 9999999")); wc.waitForBackgroundJavaScript(100); diff --git a/test/src/test/java/hudson/PluginTest.java b/test/src/test/java/hudson/PluginTest.java index 0e4192ae4bf8..79cf3703a39a 100644 --- a/test/src/test/java/hudson/PluginTest.java +++ b/test/src/test/java/hudson/PluginTest.java @@ -54,7 +54,7 @@ public class PluginTest { r.createWebClient().assertFails("plugin/matrix-auth/images/%2e%2e%2fWEB-INF/licenses.xml", HttpServletResponse.SC_BAD_REQUEST); r.createWebClient().assertFails("plugin/matrix-auth/images/%2e.%2fWEB-INF/licenses.xml", HttpServletResponse.SC_BAD_REQUEST); r.createWebClient().assertFails("plugin/matrix-auth/images/..%2f..%2f..%2f" + r.jenkins.getRootDir().getName() + "%2fsecrets%2fmaster.key", HttpServletResponse.SC_BAD_REQUEST); - r.createWebClient().assertFails("plugin/matrix-auth/" + r.jenkins.getRootDir() + "/secrets/master.key", /* ./ prepended anyway */ HttpServletResponse.SC_NOT_FOUND); + r.createWebClient().assertFails("plugin/matrix-auth/" + r.jenkins.getRootDir() + "/secrets/master.key", /* ./ prepended anyway */ Functions.isWindows() ? HttpServletResponse.SC_BAD_REQUEST : HttpServletResponse.SC_NOT_FOUND); // SECURITY-155: r.createWebClient().assertFails("plugin/matrix-auth/WEB-INF/licenses.xml", HttpServletResponse.SC_BAD_REQUEST); r.createWebClient().assertFails("plugin/matrix-auth/META-INF/MANIFEST.MF", HttpServletResponse.SC_BAD_REQUEST); diff --git a/test/src/test/java/hudson/cli/Security3315Test.java b/test/src/test/java/hudson/cli/Security3315Test.java index 83d207a49c66..c48129cdfccf 100644 --- a/test/src/test/java/hudson/cli/Security3315Test.java +++ b/test/src/test/java/hudson/cli/Security3315Test.java @@ -34,7 +34,7 @@ public static List escapeHatchValues() { public Security3315Test(String allowWs) { this.allowWs = allowWs == null ? null : Boolean.valueOf(allowWs); - this.escapeHatch = new FlagRule<>(() -> CLIAction.ALLOW_WEBSOCKET, v -> { CLIAction.ALLOW_WEBSOCKET = v; }, this.allowWs); + this.escapeHatch = new FlagRule<>(() -> CLIAction.ALLOW_WEBSOCKET, v -> CLIAction.ALLOW_WEBSOCKET = v, this.allowWs); } @Test diff --git a/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java b/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java index 60b08b0936de..3e6d0ec82196 100644 --- a/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java +++ b/test/src/test/java/hudson/console/ConsoleAnnotatorTest.java @@ -76,7 +76,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen // make sure raw console output doesn't include the garbage TextPage raw = (TextPage) r.createWebClient().goTo(b.getUrl() + "consoleText", "text/plain"); System.out.println(raw.getContent()); - String nl = System.getProperty("line.separator"); + String nl = System.lineSeparator(); assertTrue(raw.getContent().contains(nl + "---" + nl + "ooo" + nl + "ooo" + nl)); // there should be two 'ooo's @@ -96,7 +96,7 @@ public ConsoleAnnotator newInstance(FreeStyleBuild context) { } public static class DemoAnnotator extends ConsoleAnnotator { - private static final String ANNOTATE_TEXT = "ooo" + System.getProperty("line.separator"); + private static final String ANNOTATE_TEXT = "ooo" + System.lineSeparator(); @Override public ConsoleAnnotator annotate(FreeStyleBuild build, MarkupText text) { diff --git a/test/src/test/java/hudson/model/CauseTest.java b/test/src/test/java/hudson/model/CauseTest.java index 0d4caf54f61b..2484a03c52ed 100644 --- a/test/src/test/java/hudson/model/CauseTest.java +++ b/test/src/test/java/hudson/model/CauseTest.java @@ -63,9 +63,9 @@ public class CauseTest { FreeStyleProject b = j.createFreeStyleProject("b"); FreeStyleBuild early = null; FreeStyleBuild last = null; - List> futures = new ArrayList<>(); for (int i = 1; i <= 15; i++) { - last = recordFuture(b.scheduleBuild2(0, new Cause.UpstreamCause(recordFuture(a.scheduleBuild2(0, last == null ? null : new Cause.UpstreamCause(last)), futures).get())), futures).get(); + last = j.waitForCompletion(a.scheduleBuild2(0, last == null ? null : new Cause.UpstreamCause(last)).get()); + last = j.waitForCompletion(b.scheduleBuild2(0, new Cause.UpstreamCause(last)).get()); if (i == 5) { early = last; } @@ -74,9 +74,6 @@ public class CauseTest { assertTrue("keeps full history:\n" + buildXml, buildXml.contains("1")); buildXml = new XmlFile(Run.XSTREAM, new File(last.getRootDir(), "build.xml")).asString(); assertFalse("too big:\n" + buildXml, buildXml.contains("1")); - for (QueueTaskFuture future : futures) { - j.assertBuildStatusSuccess(j.waitForCompletion(future.waitForStart())); - } } @Issue("JENKINS-15747") @@ -88,16 +85,16 @@ public class CauseTest { Run last = null; for (int i = 1; i <= 10; i++) { Cause cause = last == null ? null : new Cause.UpstreamCause(last); - QueueTaskFuture next1 = recordFuture(a.scheduleBuild2(0, cause), futures); - recordFuture(a.scheduleBuild2(0, cause), futures); - cause = new Cause.UpstreamCause(next1.get()); - QueueTaskFuture next2 = recordFuture(b.scheduleBuild2(0, cause), futures); - recordFuture(b.scheduleBuild2(0, cause), futures); - cause = new Cause.UpstreamCause(next2.get()); - QueueTaskFuture next3 = recordFuture(c.scheduleBuild2(0, cause), futures); - recordFuture(c.scheduleBuild2(0, cause), futures); - last = next3.get(); + last = j.waitForCompletion(a.scheduleBuild2(0, cause).get()); + recordFuture(b.scheduleBuild2(1, cause), futures); + cause = new Cause.UpstreamCause(last); + last = j.waitForCompletion(b.scheduleBuild2(0, cause).get()); + recordFuture(c.scheduleBuild2(1, cause), futures); + cause = new Cause.UpstreamCause(last); + last = j.waitForCompletion(c.scheduleBuild2(0, cause).get()); + recordFuture(a.scheduleBuild2(1, cause), futures); } + last = j.waitForCompletion(a.scheduleBuild2(0, new Cause.UpstreamCause(last)).get()); int count = new XmlFile(Run.XSTREAM, new File(last.getRootDir(), "build.xml")).asString().split(Pattern.quote(" 100); //j.interactiveBreak(); diff --git a/test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java b/test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java index 7b1bbdb7c9bd..24c978e78f8d 100644 --- a/test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java +++ b/test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java @@ -149,8 +149,16 @@ public void doubleDots2() throws Exception { p.getBuildersList().add(new Shell("mkdir abc; touch abc/def.bin")); j.buildAndAssertSuccess(p); - // can we see it? - j.createWebClient().goTo("job/" + p.getName() + "/ws/abc%5Cdef.bin", "application/octet-stream"); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + // normal path provided by the UI succeeds + wc.goTo("job/" + p.getName() + "/ws/abc/def.bin", "application/octet-stream"); + + // suspicious path is rejected with 400 + wc.setThrowExceptionOnFailingStatusCode(false); + HtmlPage page = wc.goTo("job/" + p.getName() + "/ws/abc%5Cdef.bin"); + assertEquals(400, page.getWebResponse().getStatusCode()); + assertEquals("Error 400 Suspicious Path Character", page.getTitleText()); + } } @Test @@ -1108,37 +1116,13 @@ public void windows_cannotViewAbsolutePath() throws Exception { String content = "random data provided as fixed value"; Files.writeString(targetTmpPath, content, StandardCharsets.UTF_8); - JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false); - Page page = wc.goTo("userContent/" + targetTmpPath.toAbsolutePath() + "/*view*", null); - - MatcherAssert.assertThat(page.getWebResponse().getStatusCode(), equalTo(404)); - } - - @Test - @Issue("SECURITY-2481") - public void windows_canViewAbsolutePath_withEscapeHatch() throws Exception { - Assume.assumeTrue("can only be tested this on Windows", Functions.isWindows()); - - String originalValue = System.getProperty(DirectoryBrowserSupport.ALLOW_ABSOLUTE_PATH_PROPERTY_NAME); - System.setProperty(DirectoryBrowserSupport.ALLOW_ABSOLUTE_PATH_PROPERTY_NAME, "true"); - try { - Path targetTmpPath = Files.createTempFile("sec2481", "tmp"); - String content = "random data provided as fixed value"; - Files.writeString(targetTmpPath, content, StandardCharsets.UTF_8); - - JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false); - Page page = wc.goTo("userContent/" + targetTmpPath.toAbsolutePath() + "/*view*", null); - - MatcherAssert.assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); - MatcherAssert.assertThat(page.getWebResponse().getContentAsString(), containsString(content)); - } finally { - if (originalValue == null) { - System.clearProperty(DirectoryBrowserSupport.ALLOW_ABSOLUTE_PATH_PROPERTY_NAME); - } else { - System.setProperty(DirectoryBrowserSupport.ALLOW_ABSOLUTE_PATH_PROPERTY_NAME, originalValue); - } + try (JenkinsRule.WebClient wc = j.createWebClient()) { + // suspicious path is rejected with 400 + wc.setThrowExceptionOnFailingStatusCode(false); + HtmlPage page = wc.goTo("userContent/" + targetTmpPath.toAbsolutePath() + "/*view*"); + assertEquals(400, page.getWebResponse().getStatusCode()); + assertEquals("Error 400 Suspicious Path Character", page.getTitleText()); } - } @Test diff --git a/test/src/test/java/hudson/model/DownloadService2Test.java b/test/src/test/java/hudson/model/DownloadService2Test.java index 0b4fe687d3eb..73a15c64be6d 100644 --- a/test/src/test/java/hudson/model/DownloadService2Test.java +++ b/test/src/test/java/hudson/model/DownloadService2Test.java @@ -60,7 +60,7 @@ private static void assertRoots(String expected, String file) throws Exception { URL resource = DownloadService2Test.class.getResource(file); assertNotNull(file, resource); JSONObject json = JSONObject.fromObject(DownloadService.loadJSONHTML(resource)); - @SuppressWarnings("unchecked") Set keySet = json.keySet(); + Set keySet = json.keySet(); assertEquals(expected, new TreeSet<>(keySet).toString()); } diff --git a/test/src/test/java/hudson/model/DownloadServiceTest.java b/test/src/test/java/hudson/model/DownloadServiceTest.java index f5fc53730029..cb9337c72096 100644 --- a/test/src/test/java/hudson/model/DownloadServiceTest.java +++ b/test/src/test/java/hudson/model/DownloadServiceTest.java @@ -34,7 +34,7 @@ public void testLoadJSON() throws Exception { private static void assertRoots(String expected, URL resource) throws Exception { assertNotNull(resource); JSONObject json = JSONObject.fromObject(DownloadService.loadJSON(resource)); - @SuppressWarnings("unchecked") Set keySet = json.keySet(); + Set keySet = json.keySet(); assertEquals(expected, new TreeSet<>(keySet).toString()); } diff --git a/test/src/test/java/hudson/model/FileParameterValuePersistenceTest.java b/test/src/test/java/hudson/model/FileParameterValuePersistenceTest.java new file mode 100644 index 000000000000..660c305e055e --- /dev/null +++ b/test/src/test/java/hudson/model/FileParameterValuePersistenceTest.java @@ -0,0 +1,81 @@ +package hudson.model; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import hudson.Functions; +import hudson.tasks.BatchFile; +import hudson.tasks.Shell; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlInput; +import org.htmlunit.html.HtmlPage; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsSessionRule; + +public class FileParameterValuePersistenceTest { + + private static final String FILENAME = "file.txt"; + private static final String CONTENTS = "foobar"; + + @Rule + public JenkinsSessionRule sessions = new JenkinsSessionRule(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Issue("JENKINS-13536") + @Test + public void fileParameterValuePersistence() throws Throwable { + sessions.then(j -> { + FreeStyleProject p = j.createFreeStyleProject("p"); + p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition(FILENAME, "The file."))); + p.getBuildersList().add(Functions.isWindows() ? new BatchFile("type " + FILENAME) : new Shell("cat " + FILENAME)); + File test = tmp.newFile(); + Files.writeString(test.toPath(), CONTENTS, StandardCharsets.UTF_8); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + // ParametersDefinitionProperty/index.jelly sends a 405 + wc.setThrowExceptionOnFailingStatusCode(false); + HtmlPage page = wc.goTo("job/" + p.getName() + "/build?delay=0sec"); + assertEquals(405, page.getWebResponse().getStatusCode()); + HtmlForm form = page.getFormByName("parameters"); + HtmlInput input = form.getInputByName("file"); + input.setValue(test.getPath()); + page = j.submit(form); + assertEquals(200, page.getWebResponse().getStatusCode()); + } + FreeStyleBuild b; + while ((b = p.getLastBuild()) == null) { + Thread.sleep(100); + } + j.assertBuildStatusSuccess(j.waitForCompletion(b)); + FileParameterValue fpv = (FileParameterValue) b.getAction(ParametersAction.class).getParameter(FILENAME); + fpv.getFile2().delete(); + verifyPersistence(j); + }); + sessions.then(FileParameterValuePersistenceTest::verifyPersistence); + } + + private static void verifyPersistence(JenkinsRule j) throws Throwable { + FreeStyleProject p = j.jenkins.getItemByFullName("p", FreeStyleProject.class); + FreeStyleBuild b = p.getLastBuild(); + j.assertLogContains(CONTENTS, b); + Path saved = b.getRootDir().toPath().resolve("fileParameters").resolve(FILENAME); + assertTrue(Files.isRegularFile(saved)); + assertEquals(CONTENTS, Files.readString(saved, StandardCharsets.UTF_8)); + assertTrue(b.getWorkspace().child(FILENAME).exists()); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage page = wc.goTo(p.getUrl() + "ws"); + assertThat(page.getWebResponse().getContentAsString(), containsString(FILENAME)); + } + } +} diff --git a/test/src/test/java/hudson/model/ParametersDefinitionPropertyTest.java b/test/src/test/java/hudson/model/ParametersDefinitionPropertyTest.java index 0f06cd43a7f6..54d3b3a0c847 100644 --- a/test/src/test/java/hudson/model/ParametersDefinitionPropertyTest.java +++ b/test/src/test/java/hudson/model/ParametersDefinitionPropertyTest.java @@ -24,11 +24,19 @@ package hudson.model; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; +import java.net.URL; import java.util.Locale; import java.util.logging.Level; import net.sf.json.JSONObject; +import org.htmlunit.HttpMethod; +import org.htmlunit.WebRequest; +import org.htmlunit.WebResponse; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -92,4 +100,27 @@ public ParameterDefinition newInstance(StaplerRequest req, JSONObject formData) } + @Issue("JENKINS-66105") + @Test + public void statusCodes() throws Exception { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new FullControlOnceLoggedInAuthorizationStrategy()); + FreeStyleProject p = r.createFreeStyleProject("p"); + ParametersDefinitionProperty pdp = new ParametersDefinitionProperty(new StringParameterDefinition("K")); + p.addProperty(pdp); + p.setConcurrentBuild(true); + p.setAssignedLabel(Label.get("nonexistent")); // force it to stay in queue + JenkinsRule.WebClient wc = r.createWebClient(); + wc.withBasicApiToken("dev"); + assertThat("initially 201 Created queue item", buildWithParameters(wc, "v1").getStatusCode(), is(201)); + WebResponse rsp = buildWithParameters(wc, "v1"); + assertThat("then 303 See Other → 200 OK", rsp.getStatusCode(), is(200)); + assertThat("offers advice on API", rsp.getContentAsString(), containsString("api/json?tree=")); + assertThat("201 Created queue item for different key", buildWithParameters(wc, "v2").getStatusCode(), is(201)); + } + + private WebResponse buildWithParameters(JenkinsRule.WebClient wc, String value) throws Exception { + return wc.getPage(new WebRequest(new URL(wc.getContextPath() + "job/p/buildWithParameters?K=" + value), HttpMethod.POST)).getWebResponse(); + } + } diff --git a/test/src/test/java/hudson/model/ProjectTest.java b/test/src/test/java/hudson/model/ProjectTest.java index 82d9266e0e62..e3f2d84493e3 100644 --- a/test/src/test/java/hudson/model/ProjectTest.java +++ b/test/src/test/java/hudson/model/ProjectTest.java @@ -644,15 +644,13 @@ public void testDoDisable() throws Exception { JenkinsRule.WebClient wc = j.createWebClient(); wc.withBasicCredentials(user.getId(), "password"); - HtmlPage p = wc.goTo(project.getUrl()); - List forms = p.getForms(); - for (HtmlForm form : forms) { - if ("disable".equals(form.getAttribute("action"))) { - j.submit(form); - } - } - assertTrue("Project should be disabled.", project.isDisabled()); + HtmlPage p = wc.getPage(project, "configure"); + HtmlForm form = p.getFormByName("config"); + form.getInputByName("enable").click(); + j.submit(form); + + assertTrue("Project should be disabled.", project.isDisabled()); } @Test diff --git a/test/src/test/java/hudson/model/QueueTest.java b/test/src/test/java/hudson/model/QueueTest.java index 878b04e28df9..fa76fd88ae0d 100644 --- a/test/src/test/java/hudson/model/QueueTest.java +++ b/test/src/test/java/hudson/model/QueueTest.java @@ -78,7 +78,6 @@ import hudson.triggers.SCMTrigger.SCMTriggerCause; import hudson.triggers.TimerTrigger.TimerTriggerCause; import hudson.util.OneShotEvent; -import hudson.util.XStream2; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; @@ -103,30 +102,17 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import jenkins.model.BlockedBecauseOfBuildInProgress; import jenkins.model.Jenkins; import jenkins.model.queue.QueueIdStrategy; import jenkins.security.QueueItemAuthenticatorConfiguration; import org.acegisecurity.acls.sid.PrincipalSid; -import org.apache.commons.fileupload.FileUploadException; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.htmlunit.HttpMethod; import org.htmlunit.Page; import org.htmlunit.ScriptResult; import org.htmlunit.WebRequest; import org.htmlunit.html.DomElement; import org.htmlunit.html.DomNode; -import org.htmlunit.html.HtmlFileInput; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlFormUtil; import org.htmlunit.html.HtmlPage; import org.htmlunit.xml.XmlPage; import org.junit.Assert; @@ -294,60 +280,6 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen r.assertBuildStatusSuccess(r.waitForCompletion(b1)); } - public static final class FileItemPersistenceTestServlet extends HttpServlet { - private static final long serialVersionUID = 1L; - - @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - resp.setContentType("text/html"); - resp.getWriter().println( - "" + - "" + - "" - ); - } - - @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException { - try { - ServletFileUpload f = new ServletFileUpload(new DiskFileItemFactory()); - List v = f.parseRequest(req); - assertEquals(1, v.size()); - XStream2 xs = new XStream2(); - System.out.println(xs.toXML(v.get(0))); - } catch (FileUploadException e) { - throw new ServletException(e); - } - } - } - - @Test public void fileItemPersistence() throws Exception { - // TODO: write a synchronous connector? - byte[] testData = new byte[1024]; - for (int i = 0; i < testData.length; i++) testData[i] = (byte) i; - - - Server server = new Server(); - ServerConnector connector = new ServerConnector(server); - server.addConnector(connector); - - ServletHandler handler = new ServletHandler(); - handler.addServletWithMapping(new ServletHolder(new FileItemPersistenceTestServlet()), "/"); - server.setHandler(handler); - - server.start(); - - try { - JenkinsRule.WebClient wc = r.createWebClient(); - @SuppressWarnings("deprecation") - HtmlPage p = (HtmlPage) wc.getPage("http://localhost:" + connector.getLocalPort() + '/'); - HtmlForm f = p.getFormByName("main"); - HtmlFileInput input = f.getInputByName("test"); - input.setData(testData); - HtmlFormUtil.submit(f); - } finally { - server.stop(); - } - } - @Issue("JENKINS-33467") @Test public void foldableCauseAction() throws Exception { final OneShotEvent buildStarted = new OneShotEvent(); diff --git a/test/src/test/java/hudson/model/RunTest.java b/test/src/test/java/hudson/model/RunTest.java index c59245c78c2c..b6a145857020 100644 --- a/test/src/test/java/hudson/model/RunTest.java +++ b/test/src/test/java/hudson/model/RunTest.java @@ -157,7 +157,7 @@ private void ensureXssIsPrevented(FreeStyleProject upProject, String validationP HtmlPage htmlPage = wc.goTo(upProject.getUrl()); // trigger the tooltip display - htmlPage.executeJavaScript("document.querySelector('#buildHistory table .build-badge svg')._tippy.show()"); + htmlPage.executeJavaScript("document.querySelector('#jenkins-build-history .app-builds-container__item__inner__controls svg')._tippy.show()"); wc.waitForBackgroundJavaScript(500); ScriptResult result = htmlPage.executeJavaScript("document.querySelector('.tippy-content').innerHTML;"); Object jsResult = result.getJavaScriptResult(); diff --git a/test/src/test/java/hudson/model/Security3349Test.java b/test/src/test/java/hudson/model/Security3349Test.java new file mode 100644 index 000000000000..6de55eb653dd --- /dev/null +++ b/test/src/test/java/hudson/model/Security3349Test.java @@ -0,0 +1,80 @@ +package hudson.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import jenkins.model.Jenkins; +import org.htmlunit.html.HtmlPage; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +public class Security3349Test { + + @Rule + public JenkinsRule rule = new JenkinsRule(); + + @Rule public FlagRule skipPermissionCheck = new FlagRule<>(() -> MyViewsProperty.SKIP_PERMISSION_CHECK, x -> MyViewsProperty.SKIP_PERMISSION_CHECK = x); + + @Test + @Issue("SECURITY-3349") + public void usersCannotAccessOtherUsersViews() throws Exception { + User user = User.getOrCreateByIdOrFullName("user"); + User admin = User.getOrCreateByIdOrFullName("admin"); + + rule.jenkins.setSecurityRealm(rule.createDummySecurityRealm()); + MockAuthorizationStrategy mockAuthorizationStrategy = new MockAuthorizationStrategy(); + mockAuthorizationStrategy.grant(Jenkins.READ, View.READ).everywhere().to("user"); + mockAuthorizationStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin"); + rule.jenkins.setAuthorizationStrategy(mockAuthorizationStrategy); + + MyViewsProperty prop1 = new MyViewsProperty(null); + MyView usersView = new MyView("User's view", prop1); + user.addProperty(prop1); + prop1.setUser(user); + prop1.addView(usersView); + + MyViewsProperty prop2 = new MyViewsProperty(null); + MyView adminsView = new MyView("Admin's view", prop2); + admin.addProperty(prop2); + prop2.setUser(admin); + prop2.addView(adminsView); + + try (JenkinsRule.WebClient wc = rule.createWebClient()) { + wc.setThrowExceptionOnFailingStatusCode(false); + wc.login("user"); + + HtmlPage adminViews = wc.goTo("user/admin/my-views/view/all/"); + assertEquals(403, adminViews.getWebResponse().getStatusCode()); + + HtmlPage adminUserPage = wc.goTo("user/admin/"); + assertFalse(adminUserPage.getWebResponse().getContentAsString().contains("My Views")); + + HtmlPage userViews = wc.goTo("user/user/my-views/view/all/"); + assertEquals(200, userViews.getWebResponse().getStatusCode()); + + HtmlPage userUserPage = wc.goTo("user/user/"); + assertTrue(userUserPage.getWebResponse().getContentAsString().contains("My Views")); + + wc.login("admin"); + + adminViews = wc.goTo("user/admin/my-views/view/all/"); + assertEquals(200, adminViews.getWebResponse().getStatusCode()); + userViews = wc.goTo("user/user/my-views/view/all/"); + assertEquals(200, userViews.getWebResponse().getStatusCode()); + + MyViewsProperty.SKIP_PERMISSION_CHECK = true; + + wc.login("user"); + adminViews = wc.goTo("user/admin/my-views/view/all/"); + assertEquals(200, adminViews.getWebResponse().getStatusCode()); + adminUserPage = wc.goTo("user/admin/"); + assertTrue(adminUserPage.getWebResponse().getContentAsString().contains("My Views")); + + } + } +} diff --git a/test/src/test/java/hudson/model/SlaveTest.java b/test/src/test/java/hudson/model/SlaveTest.java index b66130bfc4ea..f7177b9d08fc 100644 --- a/test/src/test/java/hudson/model/SlaveTest.java +++ b/test/src/test/java/hudson/model/SlaveTest.java @@ -160,7 +160,7 @@ public void shouldNotEscapeJnlpSlavesResources() throws Exception { private void assertJnlpJarUrlFails(@NonNull Slave slave, @NonNull String url) throws Exception { // Raw access to API Slave.JnlpJar jnlpJar = slave.getComputer().getJnlpJars(url); - assertThrows(MalformedURLException.class, () -> jnlpJar.getURL()); + assertThrows(MalformedURLException.class, jnlpJar::getURL); } private void assertJnlpJarUrlIsAllowed(@NonNull Slave slave, @NonNull String url) throws Exception { diff --git a/test/src/test/java/hudson/model/UpdateSiteTest.java b/test/src/test/java/hudson/model/UpdateSiteTest.java index 5902387c2e83..629c7c7f46e8 100644 --- a/test/src/test/java/hudson/model/UpdateSiteTest.java +++ b/test/src/test/java/hudson/model/UpdateSiteTest.java @@ -55,16 +55,19 @@ import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import jenkins.model.Jenkins; import jenkins.security.UpdateSiteWarningsConfiguration; import jenkins.security.UpdateSiteWarningsMonitor; import org.apache.commons.io.FilenameUtils; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.Callback; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -115,19 +118,21 @@ public void setUpWebServer() throws Exception { server = new Server(); ServerConnector connector = new ServerConnector(server); server.addConnector(connector); - server.setHandler(new AbstractHandler() { + server.setHandler(new Handler.Abstract() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { + public boolean handle(Request request, Response response, Callback callback) throws IOException { + String target = request.getHttpURI().getPath(); if (target.startsWith(RELATIVE_BASE)) { target = target.substring(RELATIVE_BASE.length()); } String responseBody = getResource(target); if (responseBody != null) { - baseRequest.setHandled(true); - response.setContentType("text/plain; charset=utf-8"); - response.setStatus(HttpServletResponse.SC_OK); - response.getOutputStream().write(responseBody.getBytes(StandardCharsets.UTF_8)); + response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8"); + response.setStatus(HttpStatus.OK_200); + Content.Sink.write(response, true, responseBody, callback); + return true; } + return false; } }); server.start(); diff --git a/test/src/test/java/hudson/model/UserPropertyTest.java b/test/src/test/java/hudson/model/UserPropertyTest.java index 0d2b7d5b1374..91c977d4ec28 100644 --- a/test/src/test/java/hudson/model/UserPropertyTest.java +++ b/test/src/test/java/hudson/model/UserPropertyTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.htmlunit.html.HtmlFormUtil.submit; import static org.junit.Assert.assertNotNull; import java.io.File; @@ -35,12 +36,17 @@ public class UserPropertyTest { @Rule public JenkinsRule j = new JenkinsRule(); + public User configRoundtrip(User u) throws Exception { + submit(j.createWebClient().goTo(u.getUrl()+"/account/").getFormByName("config")); + return u; + } + @Test @Issue("JENKINS-9062") public void test() throws Exception { User u = User.get("foo"); u.addProperty(new UserProperty1()); - j.configRoundtrip(u); + configRoundtrip(u); for (UserProperty p : u.getAllProperties()) assertNotNull(p); } @@ -82,7 +88,7 @@ public void nestedUserReference() throws Exception { List fileLines = Files.readAllLines(testFile.toPath(), StandardCharsets.US_ASCII); assertThat(fileLines, hasSize(1)); - j.configRoundtrip(user); + configRoundtrip(user); user = User.get("nestedUserReference", false, Collections.emptyMap()); assertThat("nested reference should exist after user configuration change", user, nestedUserSet()); diff --git a/test/src/test/java/hudson/model/UserTest.java b/test/src/test/java/hudson/model/UserTest.java index 316c5fe89e65..b27adbdc15e7 100644 --- a/test/src/test/java/hudson/model/UserTest.java +++ b/test/src/test/java/hudson/model/UserTest.java @@ -64,12 +64,14 @@ import jenkins.model.IdStrategy; import jenkins.model.Jenkins; import jenkins.security.ApiTokenProperty; +import org.htmlunit.FailingHttpStatusCodeException; import org.htmlunit.WebAssert; import org.htmlunit.WebRequest; import org.htmlunit.WebResponse; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlPage; import org.htmlunit.util.WebConnectionWrapper; +import org.jenkinsci.plugins.matrixauth.PermissionEntry; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -256,7 +258,7 @@ public void testAddAndGetProperty() throws Exception { user.addProperty(prop); assertNotNull("User should have SomeUserProperty property.", user.getProperty(SomeUserProperty.class)); assertEquals("UserProperty1 should be assigned to its descriptor", prop, user.getProperties().get(prop.getDescriptor())); - assertTrue("User should should contain SomeUserProperty.", user.getAllProperties().contains(prop)); + assertTrue("User should contain SomeUserProperty.", user.getAllProperties().contains(prop)); } j.jenkins.reload(); { @@ -403,20 +405,23 @@ public void testDoConfigSubmit() throws Exception { User user = realm.createAccount("John Smith", "password"); User user2 = realm.createAccount("John Smith2", "password"); user2.save(); - auth.add(Jenkins.ADMINISTER, user.getId()); - auth.add(Jenkins.READ, user2.getId()); + auth.add(Jenkins.ADMINISTER, PermissionEntry.user(user.getId())); + auth.add(Jenkins.READ, PermissionEntry.user(user2.getId())); SecurityContextHolder.getContext().setAuthentication(user.impersonate2()); - HtmlForm form = j.createWebClient().withBasicCredentials(user.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); + HtmlForm form = j.createWebClient().withBasicCredentials(user.getId(), "password").goTo(user2.getUrl() + "/account/").getFormByName("config"); form.getInputByName("_.fullName").setValue("Alice Smith"); j.submit(form); assertEquals("User should have full name Alice Smith.", "Alice Smith", user2.getFullName()); SecurityContextHolder.getContext().setAuthentication(user2.impersonate2()); - assertThrows("User should not have permission to configure another user.", AccessDeniedException3.class, () -> user.doConfigSubmit(null, null)); - form = j.createWebClient().withBasicCredentials(user2.getId(), "password").goTo(user2.getUrl() + "/configure").getFormByName("config"); + try (JenkinsRule.WebClient webClient = j.createWebClient().withBasicCredentials(user2.getId(), "password")) { + FailingHttpStatusCodeException failingHttpStatusCodeException = assertThrows("User should not have permission to configure another user.", FailingHttpStatusCodeException.class, () -> webClient.goTo(user.getUrl() + "/account/")); + assertThat(failingHttpStatusCodeException.getStatusCode(), is(403)); + form = webClient.goTo(user2.getUrl() + "/account/").getFormByName("config"); + form.getInputByName("_.fullName").setValue("John"); + j.submit(form); + } - form.getInputByName("_.fullName").setValue("John"); - j.submit(form); - assertEquals("User should be albe to configure himself.", "John", user2.getFullName()); + assertEquals("User should be able to configure himself.", "John", user2.getFullName()); } @@ -771,7 +776,7 @@ public WebResponse getResponse(WebRequest request) throws IOException { return r; } }; - wc.login("alice").goTo("me/configure"); + wc.login("alice").goTo("me/account/"); assertThat(failingResources, empty()); } diff --git a/test/src/test/java/hudson/model/ViewTest.java b/test/src/test/java/hudson/model/ViewTest.java index cf79bffb3d47..4355d82ad063 100644 --- a/test/src/test/java/hudson/model/ViewTest.java +++ b/test/src/test/java/hudson/model/ViewTest.java @@ -83,6 +83,7 @@ import org.htmlunit.html.HtmlLabel; import org.htmlunit.html.HtmlPage; import org.htmlunit.javascript.host.html.HTMLElement; +import org.htmlunit.javascript.host.svg.SVGElement; import org.htmlunit.util.NameValuePair; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; @@ -896,17 +897,36 @@ public void shouldNotAllowInconsistentViewName() throws IOException { @Test public void newJob_iconClassName() throws Exception { + + CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class); + customizableTLID.customId = "with_Icon"; + customizableTLID.customIconClassName = "icon-freestyle-project"; + JenkinsRule.WebClient wc = j.createWebClient(); HtmlPage page = wc.goTo("view/all/newJob"); - Object resultSrc = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon img').src").getJavaScriptResult(); + Object resultSrc = page.executeJavaScript("document.querySelector('." + customizableTLID.customId + " .icon img').src").getJavaScriptResult(); + assertThat(resultSrc, instanceOf(String.class)); String resultSrcString = (String) resultSrc; assertThat(resultSrcString, containsString("48x48")); assertThat(resultSrcString, containsString("freestyleproject.png")); } + @Test + public void newJob_svg() throws Exception { + + JenkinsRule.WebClient wc = j.createWebClient(); + + HtmlPage page = wc.goTo("view/all/newJob"); + + Object result = page.executeJavaScript("document.querySelector('.hudson_model_FreeStyleProject .icon svg')").getJavaScriptResult(); + assertThat(result, instanceOf(SVGElement.class)); + SVGElement svg = (SVGElement) result; + assertThat(svg.getClassName_js(), is("icon-xlg")); + } + @Test public void newJob_twoLetterIcon() throws Exception { CustomizableTLID customizableTLID = j.jenkins.getExtensionList(TopLevelItemDescriptor.class).get(CustomizableTLID.class); diff --git a/test/src/test/java/hudson/model/WorkspaceCleanupThreadTest.java b/test/src/test/java/hudson/model/WorkspaceCleanupThreadTest.java index c62fe7742f72..57f4946e7978 100644 --- a/test/src/test/java/hudson/model/WorkspaceCleanupThreadTest.java +++ b/test/src/test/java/hudson/model/WorkspaceCleanupThreadTest.java @@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import jenkins.MasterToSlaveFileCallable; +import jenkins.model.Jenkins; import org.junit.Assume; import org.junit.Rule; import org.junit.Test; @@ -181,12 +182,28 @@ public void deleteTemporaryDirectory() throws Exception { FilePath ws = createOldWorkspaceOn(r.jenkins, p); FilePath tmp = WorkspaceList.tempDir(ws); tmp.child("stuff").write("content", null); + tmp.act(new Touch(0)); createOldWorkspaceOn(r.createOnlineSlave(), p); performCleanup(); assertFalse(ws.exists()); assertFalse("temporary directory should be cleaned up as well", tmp.exists()); } + @Issue("JENKINS-65829") + @Test + public void deleteSoleLibsDirectory() throws Exception { + FreeStyleProject p = r.createFreeStyleProject(); + FilePath jobWs = Jenkins.get().getWorkspaceFor(p); + FilePath libsWs = jobWs.withSuffix(WorkspaceList.COMBINATOR + "libs"); + libsWs.child("test-libs").write("content", null); + libsWs.act(new Touch(0)); + assertFalse(jobWs.exists()); + assertTrue(libsWs.exists()); + performCleanup(); + assertFalse(jobWs.exists()); + assertFalse("libs directory should be cleaned up as well", libsWs.exists()); + } + private FilePath createOldWorkspaceOn(Node slave, FreeStyleProject p) throws Exception { p.setAssignedNode(slave); FreeStyleBuild b1 = r.buildAndAssertSuccess(p); @@ -197,6 +214,13 @@ private FilePath createOldWorkspaceOn(Node slave, FreeStyleProject p) throws Exc return ws; } + private FilePath createOldLibsWorkspace(FreeStyleProject p) throws IOException, InterruptedException { + FilePath libsWs = Jenkins.get().getWorkspaceFor(p).withSuffix(WorkspaceList.COMBINATOR + "libs"); + libsWs.child("test-libs").write("content", null); + libsWs.act(new Touch(0)); + return libsWs; + } + private void performCleanup() throws InterruptedException, IOException { new WorkspaceCleanupThread().execute(StreamTaskListener.fromStdout()); } diff --git a/test/src/test/java/hudson/model/listeners/ItemListenerTest.java b/test/src/test/java/hudson/model/listeners/ItemListenerTest.java index 6209790f1a97..b7147d3ebfe5 100644 --- a/test/src/test/java/hudson/model/listeners/ItemListenerTest.java +++ b/test/src/test/java/hudson/model/listeners/ItemListenerTest.java @@ -35,6 +35,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; /** @@ -58,6 +59,12 @@ public void setUp() { @Override public void onCopied(Item src, Item item) { events.append('Y'); } + + @Override + public void onUpdated(Item item) { + events.append('U'); + } + }; ItemListener.all().add(0, listener); } @@ -71,4 +78,18 @@ public void onCreatedViaCLI() { assertNotNull("job should be created: " + result, j.jenkins.getItem("testJob")); assertEquals("onCreated event should be triggered: " + result, "C", events.toString()); } + + @Issue("JENKINS-64553") + @Test + public void onUpdatedViaCLI() { + CLICommandInvoker.Result result = new CLICommandInvoker(j, "create-job"). + withStdin(new ByteArrayInputStream("".getBytes(Charset.defaultCharset()))). + invokeWithArgs("testJob"); + assertThat(result, CLICommandInvoker.Matcher.succeeded()); + result = new CLICommandInvoker(j, "update-job"). + withStdin(new ByteArrayInputStream("".getBytes(Charset.defaultCharset()))). + invokeWithArgs("testJob"); + assertThat(result, CLICommandInvoker.Matcher.succeeded()); + assertEquals("onUpdated event should be triggered: " + result, "CU", events.toString()); + } } diff --git a/test/src/test/java/hudson/model/queue/BuildKeepsRunningWhenFaultySubTasksTest.java b/test/src/test/java/hudson/model/queue/BuildKeepsRunningWhenFaultySubTasksTest.java index c296c2bd6871..7bb9b4d442c6 100644 --- a/test/src/test/java/hudson/model/queue/BuildKeepsRunningWhenFaultySubTasksTest.java +++ b/test/src/test/java/hudson/model/queue/BuildKeepsRunningWhenFaultySubTasksTest.java @@ -107,6 +107,7 @@ public Label getAssignedLabel() { return null; } + @Deprecated @Override public Node getLastBuiltOn() { return null; diff --git a/test/src/test/java/hudson/scm/AbstractScmTagActionTest.java b/test/src/test/java/hudson/scm/AbstractScmTagActionTest.java index 4fe53f261657..1df03bfacd64 100644 --- a/test/src/test/java/hudson/scm/AbstractScmTagActionTest.java +++ b/test/src/test/java/hudson/scm/AbstractScmTagActionTest.java @@ -66,7 +66,7 @@ private String buildAndExtractTooltipAttribute(FreeStyleProject p) throws Except HtmlPage page = wc.getPage(p); - DomElement buildHistory = page.getElementById("buildHistory"); + DomElement buildHistory = page.getElementById("buildHistoryPage"); DomNodeList imgs = buildHistory.getElementsByTagName("img"); HtmlImage tagImage = (HtmlImage) imgs.stream() .filter(i -> i.getAttribute("class").contains("icon-save")) diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java index 155e9cab1adc..ab1352807117 100644 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java +++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmFIPSTest.java @@ -25,25 +25,30 @@ package hudson.security; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertThrows; +import hudson.logging.LogRecorder; +import hudson.logging.LogRecorderManager; import hudson.model.User; import hudson.security.HudsonPrivateSecurityRealm.Details; +import java.lang.reflect.Method; +import java.util.List; import java.util.logging.Level; +import java.util.logging.LogRecord; +import jenkins.model.Jenkins; import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.htmlunit.FailingHttpStatusCodeException; -import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestRule; -import org.jvnet.hudson.test.FlagRule; import org.jvnet.hudson.test.For; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule.WebClient; -import org.jvnet.hudson.test.LoggerRule; +import org.jvnet.hudson.test.RealJenkinsRule; @For(HudsonPrivateSecurityRealm.class) @@ -52,18 +57,18 @@ public class HudsonPrivateSecurityRealmFIPSTest { // the jbcrypt encoded for of "a" without the quotes private static final String JBCRYPT_ENCODED_PASSWORD = "#jbcrypt:$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe"; - @ClassRule - // do not use the FIPS140 class here as that initializes the field before we set the property! - public static TestRule flagRule = FlagRule.systemProperty("jenkins.security.FIPS140.COMPLIANCE", "true"); + private static final String LOG_RECORDER_NAME = "HPSR_LOG_RECORDER"; @Rule - public LoggerRule lr = new LoggerRule().record(HudsonPrivateSecurityRealm.class, Level.WARNING).capture(5); - - @Rule - public JenkinsRule j = new JenkinsRule(); + public RealJenkinsRule rjr = new RealJenkinsRule().includeTestClasspathPlugins(false) + .javaOptions("-Xmx256M", "-Djenkins.security.FIPS140.COMPLIANCE=true"); @Test - public void generalLogin() throws Exception { + public void generalLogin() throws Throwable { + rjr.then(HudsonPrivateSecurityRealmFIPSTest::generalLoginStep); + } + + private static void generalLoginStep(JenkinsRule j) throws Exception { HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null); j.jenkins.setSecurityRealm(securityRealm); @@ -75,37 +80,50 @@ public void generalLogin() throws Exception { String hashedPassword = u1.getProperty(Details.class).getPassword(); assertThat(hashedPassword, startsWith("$PBKDF2$HMACSHA512:210000:")); - WebClient wc = j.createWebClient(); - wc.login("user", "password"); - - assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user", "wrongPass")); + try (WebClient wc = j.createWebClient()) { + wc.login("user", "password"); + assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user", "wrongPass")); + } } @Test - public void userCreationWithHashedPasswords() throws Exception { + public void userCreationWithHashedPasswords() throws Throwable { + rjr.then(HudsonPrivateSecurityRealmFIPSTest::userCreationWithHashedPasswordsStep); + } + + private static void userCreationWithHashedPasswordsStep(JenkinsRule j) throws Exception { + setupLogRecorder(); HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null); j.jenkins.setSecurityRealm(securityRealm); // "password" after it has gone through the KDF securityRealm.createAccountWithHashedPassword("user_hashed", "$PBKDF2$HMACSHA512:210000:ffbb207b847010af98cdd2b09c79392c$f67c3b985daf60db83a9088bc2439f7b77016d26c1439a9877c4f863c377272283ce346edda4578a5607ea620a4beb662d853b800f373297e6f596af797743a6"); - WebClient wc = j.createWebClient(); - // login should succeed - wc.login("user_hashed", "password"); - - assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user_hashed", "password2")); - assertThat(lr, not(hasIncorrectHashingLogEntry())); + try (WebClient wc = j.createWebClient()) { + // login should succeed + wc.login("user_hashed", "password"); + assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user_hashed", "password2")); + } + assertThat(getLogRecords(), not(hasItem(incorrectHashingLogEntry()))); } @Test - public void userLoginAfterEnablingFIPS() throws Exception { + public void userLoginAfterEnablingFIPS() throws Throwable { + rjr.then(HudsonPrivateSecurityRealmFIPSTest::userLoginAfterEnablingFIPSStep); + } + + private static void userLoginAfterEnablingFIPSStep(JenkinsRule j) throws Exception { + setupLogRecorder(); HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null); j.jenkins.setSecurityRealm(securityRealm); User u1 = securityRealm.createAccount("user", "a"); u1.setFullName("A User"); // overwrite the password property using an password created using an incorrect algorithm - u1.addProperty(Details.fromHashedPassword(JBCRYPT_ENCODED_PASSWORD)); + Method m = Details.class.getDeclaredMethod("fromHashedPassword", String.class); + m.setAccessible(true); + Details d = (Details) m.invoke(null, JBCRYPT_ENCODED_PASSWORD); + u1.addProperty(d); u1.save(); assertThat(u1.getProperty(Details.class).getPassword(), is(JBCRYPT_ENCODED_PASSWORD)); @@ -113,11 +131,16 @@ public void userLoginAfterEnablingFIPS() throws Exception { try (WebClient wc = j.createWebClient()) { assertThrows(FailingHttpStatusCodeException.class, () -> wc.login("user", "a")); } - assertThat(lr, hasIncorrectHashingLogEntry()); + assertThat(getLogRecords(), hasItem(incorrectHashingLogEntry())); } @Test - public void userCreationWithJBCryptPasswords() throws Exception { + public void userCreationWithJBCryptPasswords() throws Throwable { + rjr.then(HudsonPrivateSecurityRealmFIPSTest::userCreationWithJBCryptPasswordsStep); + + } + + private static void userCreationWithJBCryptPasswordsStep(JenkinsRule j) throws Exception { HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null); IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, @@ -126,8 +149,21 @@ public void userCreationWithJBCryptPasswords() throws Exception { is("The hashed password was hashed with an incorrect algorithm. Jenkins is expecting $PBKDF2")); } - private static Matcher hasIncorrectHashingLogEntry() { - return LoggerRule.recorded(is( - "A password appears to be stored (or is attempting to be stored) that was created with a different hashing/encryption algorithm, check the FIPS-140 state of the system has not changed inadvertently")); + private static Matcher incorrectHashingLogEntry() { + return Matchers.hasProperty("message", + is("A password appears to be stored (or is attempting to be stored) that was created with a different hashing/encryption algorithm, check the FIPS-140 state of the system has not changed inadvertently")); + } + + private static List getLogRecords() { + return Jenkins.get().getLog().getLogRecorder(LOG_RECORDER_NAME).getLogRecords(); + } + + private static void setupLogRecorder() { + LogRecorderManager lrm = Jenkins.get().getLog(); + LogRecorder lr = new LogRecorder(LOG_RECORDER_NAME); + LogRecorder.Target target = new LogRecorder.Target(HudsonPrivateSecurityRealm.class.getName(), Level.WARNING); + lr.setLoggers(List.of(target)); + + lrm.getRecorders().add(lr); } } diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java index d0f58f92d5ec..92745bf5f9b0 100644 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java +++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java @@ -651,7 +651,7 @@ public void changingPassword_mustInvalidateAllSessions() throws Exception { wc_anotherTab.login(alice.getId()); assertUserConnected(wc_anotherTab, alice.getId()); - HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure"); + HtmlPage configurePage = wc.goTo(alice.getUrl() + "/security/"); HtmlPasswordInput password1 = configurePage.getElementByName("user.password"); HtmlPasswordInput password2 = configurePage.getElementByName("user.password2"); @@ -683,7 +683,7 @@ public void notChangingPassword_hasNoImpactOnSeed() throws Exception { wc_anotherTab.login(alice.getId()); assertUserConnected(wc_anotherTab, alice.getId()); - HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure"); + HtmlPage configurePage = wc.goTo(alice.getUrl() + "/security/"); // not changing password this time HtmlForm form = configurePage.getFormByName("config"); j.submit(form); @@ -713,7 +713,7 @@ public void changingPassword_withSeedDisable_hasNoImpact() throws Exception { wc_anotherTab.login(alice.getId()); assertUserConnected(wc_anotherTab, alice.getId()); - HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure"); + HtmlPage configurePage = wc.goTo(alice.getUrl() + "/security/"); HtmlPasswordInput password1 = configurePage.getElementByName("user.password"); HtmlPasswordInput password2 = configurePage.getElementByName("user.password2"); diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java index 544e8383de8c..557231f5d140 100644 --- a/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java +++ b/test/src/test/java/hudson/slaves/JNLPLauncherRealTest.java @@ -49,13 +49,14 @@ public class JNLPLauncherRealTest { private static final String STATIC_AGENT_NAME = "static"; - @Rule public RealJenkinsRule rr = new RealJenkinsRule().includeTestClasspathPlugins(false).withColor(PrefixedOutputStream.Color.BLUE); + @Rule public RealJenkinsRule rr = new RealJenkinsRule().withColor(PrefixedOutputStream.Color.BLUE); @Rule public InboundAgentRule iar = new InboundAgentRule(); @Issue("JEP-230") @Test public void smokes() throws Throwable { - /* Since RealJenkinsRuleInit.jpi will load detached plugins, to reproduce a failure use: + /* Since RealJenkinsRuleInit.jpi will load detached and test scope plugins, to reproduce a failure use: + rr.includeTestClasspathPlugins(false); FileUtils.touch(new File(rr.getHome(), "plugins/instance-identity.jpi.disabled")); */ then(false); diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java index e71b2fbcce9c..330d241c4a68 100644 --- a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java +++ b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java @@ -27,6 +27,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -112,6 +113,31 @@ public void testNoWorkDirMigration() { jnlpLauncher.getWorkDirSettings().isDisabled()); } + @Issue("JENKINS-73011") + @SuppressWarnings("deprecation") + @Test + public void deprecatedFields() throws Exception { + var launcher = new JNLPLauncher(); + launcher.setWebSocket(true); + launcher.setWorkDirSettings(new RemotingWorkDirSettings(false, null, "remoting2", false)); + launcher.setTunnel("someproxy"); + var agent = j.createSlave(); + agent.setLauncher(launcher); + agent = j.configRoundtrip(agent); + launcher = (JNLPLauncher) agent.getLauncher(); + assertThat(launcher.isWebSocket(), is(true)); + assertThat(launcher.getWorkDirSettings().getInternalDir(), is("remoting2")); + assertThat(launcher.getTunnel(), is("someproxy")); + launcher = new JNLPLauncher(); + launcher.setWebSocket(true); + agent.setLauncher(launcher); + agent = j.configRoundtrip(agent); + launcher = (JNLPLauncher) agent.getLauncher(); + assertThat(launcher.isWebSocket(), is(true)); + assertThat(launcher.getWorkDirSettings().getInternalDir(), is("remoting")); + assertThat(launcher.getTunnel(), nullValue()); + } + @Test public void testDefaults() { assertFalse("Work directory enabled by default", new JNLPLauncher().getWorkDirSettings().isDisabled()); diff --git a/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java b/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java index 2157e89492d7..6ceca40aba88 100644 --- a/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java +++ b/test/src/test/java/hudson/tasks/ArtifactArchiverTest.java @@ -26,6 +26,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -51,6 +52,8 @@ import hudson.slaves.DumbSlave; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.HttpURLConnection; import java.util.Collections; import java.util.Comparator; @@ -61,6 +64,7 @@ import org.hamcrest.Matchers; import org.jenkinsci.plugins.structs.describable.DescribableModel; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.BuildWatcher; @@ -392,6 +396,59 @@ public void testDefaultExcludesOff() throws Exception { assertTrue(artifacts.child("dir").child(".svn").child("file").exists()); } + @Ignore("Test is too slow and requires a lot of disk space") + @Issue("JENKINS-10629") + @Test + public void testLargeArchiveFromAgent() throws Exception { + final String filename = "large"; + final long size = 10L * 1024L * 1024L * 1024L; // 10 GB + + Slave agent = j.createOnlineSlave(); + FreeStyleProject project = j.createFreeStyleProject(); + project.setAssignedNode(agent); + project.getBuildersList().add(new TestBuilder() { + @Override + public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { + FilePath filePath = build.getWorkspace().child(filename); + try (OutputStream os = filePath.write()) { + // Create byte array and fill it with data + byte[] megabyte = new byte[1024 * 1024]; + for (int i = 0; i < megabyte.length; i++) { + megabyte[i] = (byte) (i % 128); + } + // Fill file with 1 MB chunks + for (int i = 0; i < size / megabyte.length; i++) { + os.write(megabyte); + } + } + return true; + } + }); + project.getPublishersList().add(new ArtifactArchiver(filename)); + + // Assert that the build succeeded + FreeStyleBuild build = j.buildAndAssertSuccess(project); + VirtualFile virtualFile = build.getArtifactManager().root().child(filename); + + // Assert that the artifact was copied + assertTrue(virtualFile.exists()); + + // Assert that it has the right size + assertEquals(size, virtualFile.length()); + + // Assert that the data at the end of the file is the expected data + try (InputStream is = virtualFile.open()) { + is.skip(size - 1024 * 1024); + byte[] expected = new byte[1024 * 1024]; + for (int i = 0; i < expected.length; i++) { + expected[i] = (byte) (i % 128); + } + byte[] actual = new byte[1024 * 1024]; + is.read(actual); + assertArrayEquals(expected, actual); + } + } + @LocalData @Test public void latestOnlyMigration() throws Exception { FreeStyleProject p = j.jenkins.getItemByFullName("sample", FreeStyleProject.class); diff --git a/test/src/test/java/hudson/tasks/LogRotatorTest.java b/test/src/test/java/hudson/tasks/LogRotatorTest.java index 8a408b758b01..b72e837dbd86 100644 --- a/test/src/test/java/hudson/tasks/LogRotatorTest.java +++ b/test/src/test/java/hudson/tasks/LogRotatorTest.java @@ -68,7 +68,7 @@ public class LogRotatorTest { @Test public void successVsFailure() throws Exception { FreeStyleProject project = j.createFreeStyleProject(); - project.setLogRotator(new LogRotator(-1, 2, -1, -1)); + project.setBuildDiscarder(new LogRotator(-1, 2, -1, -1)); j.buildAndAssertSuccess(project); // #1 project.getBuildersList().replaceBy(Set.of(new FailureBuilder())); j.buildAndAssertStatus(Result.FAILURE, project); // #2 @@ -82,11 +82,25 @@ public void successVsFailure() throws Exception { assertEquals(3, numberOf(project.getLastFailedBuild())); } + @Test + public void successVsFailureWithRemoveLastBuild() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + LogRotator logRotator = new LogRotator(-1, 1, -1, -1); + logRotator.setRemoveLastBuild(true); + project.setBuildDiscarder(logRotator); + project.getPublishersList().replaceBy(Set.of(new TestsFail())); + j.buildAndAssertStatus(Result.UNSTABLE, project); // #1 + project.getBuildersList().replaceBy(Set.of(new FailureBuilder())); + j.buildAndAssertStatus(Result.FAILURE, project); // #2 + assertNull(project.getBuildByNumber(1)); + assertEquals(2, numberOf(project.getLastFailedBuild())); + } + @Test @Issue("JENKINS-2417") public void stableVsUnstable() throws Exception { FreeStyleProject project = j.createFreeStyleProject(); - project.setLogRotator(new LogRotator(-1, 2, -1, -1)); + project.setBuildDiscarder(new LogRotator(-1, 2, -1, -1)); j.buildAndAssertSuccess(project); // #1 project.getPublishersList().replaceBy(Set.of(new TestsFail())); j.buildAndAssertStatus(Result.UNSTABLE, project); // #2 @@ -98,11 +112,28 @@ public void stableVsUnstable() throws Exception { assertNull(project.getBuildByNumber(2)); } + @Test + public void stableVsUnstableWithRemoveLastBuild() throws Exception { + FreeStyleProject project = j.createFreeStyleProject(); + LogRotator logRotator = new LogRotator(-1, 1, -1, -1); + logRotator.setRemoveLastBuild(true); + project.setBuildDiscarder(logRotator); + j.buildAndAssertSuccess(project); // #1 + project.getPublishersList().replaceBy(Set.of(new TestsFail())); + j.buildAndAssertStatus(Result.UNSTABLE, project); // #2 + project.getBuildersList().replaceBy(Set.of(new FailureBuilder())); + j.buildAndAssertStatus(Result.FAILURE, project); // #3 + assertNull(project.getBuildByNumber(1)); + assertNull(project.getBuildByNumber(2)); + assertEquals(-1, numberOf(project.getLastSuccessfulBuild())); + assertEquals(3, numberOf(project.getLastBuild())); + } + @Test @Issue("JENKINS-834") public void artifactDelete() throws Exception { FreeStyleProject project = j.createFreeStyleProject(); - project.setLogRotator(new LogRotator(-1, 6, -1, 2)); + project.setBuildDiscarder(new LogRotator(-1, 6, -1, 2)); project.getPublishersList().replaceBy(Set.of(new ArtifactArchiver("f", "", true, false))); j.buildAndAssertStatus(Result.FAILURE, project); // #1 assertFalse(project.getBuildByNumber(1).getHasArtifacts()); diff --git a/test/src/test/java/hudson/tasks/MavenTest.java b/test/src/test/java/hudson/tasks/MavenTest.java index dbf745b672d9..bd366adce71d 100644 --- a/test/src/test/java/hudson/tasks/MavenTest.java +++ b/test/src/test/java/hudson/tasks/MavenTest.java @@ -66,7 +66,6 @@ import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.ToolInstallations; -import org.jvnet.hudson.test.recipes.WithTimeout; import org.kohsuke.stapler.jelly.JellyFacet; /** @@ -191,7 +190,7 @@ private void verify() throws Exception { assertNotNull(isp.installers.get(MavenInstaller.class)); } - @Test @WithTimeout(500) public void sensitiveParameters() throws Exception { + @Test public void sensitiveParameters() throws Exception { FreeStyleProject project = j.createFreeStyleProject(); ParametersDefinitionProperty pdb = new ParametersDefinitionProperty( new StringParameterDefinition("string", "defaultValue", "string description"), diff --git a/test/src/test/java/hudson/util/FormFieldValidatorTest.java b/test/src/test/java/hudson/util/FormFieldValidatorTest.java index bdbb76946288..6e1b52c45c58 100644 --- a/test/src/test/java/hudson/util/FormFieldValidatorTest.java +++ b/test/src/test/java/hudson/util/FormFieldValidatorTest.java @@ -25,13 +25,19 @@ package hudson.util; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + import hudson.model.AbstractProject; import hudson.model.FreeStyleProject; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Builder; import hudson.tasks.Publisher; +import hudson.tasks.Recorder; import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; import org.htmlunit.ScriptResult; import org.htmlunit.WebResponseListener; import org.htmlunit.html.HtmlPage; @@ -42,6 +48,7 @@ import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.xml.sax.SAXException; @@ -134,7 +141,7 @@ public boolean isApplicable(Class jobType) { @Issue("JENKINS-3382") public void negative() throws Exception { BrokenFormValidatorBuilder.DescriptorImpl d = new BrokenFormValidatorBuilder.DescriptorImpl(); - Publisher.all().add(d); + Recorder.all().add(d); try { FreeStyleProject p = j.createFreeStyleProject(); p.getPublishersList().add(new BrokenFormValidatorBuilder()); @@ -152,4 +159,120 @@ public void negative() throws Exception { Publisher.all().remove(d); } } + + @Issue("JENKINS-73404") + @Test + public void testValidationforComponents() throws Exception { + FreeStyleProject p = j.createFreeStyleProject(); + p.getBuildersList().add(new ValidatingDescribable()); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + HtmlPage page = wc.getPage(p, "configure"); + assertThat(page.asNormalizedText(), allOf( + containsString("FormValidation: Password (empty)"), + containsString("FormValidation: Password (populated)"), + containsString("FormValidation: Textarea"), + containsString("FormValidation: SecretTextarea (empty)"), + containsString("FormValidation: SecretTextarea (populated)"))); + + } + + } + + public static class ValidatingDescribable extends Builder { + + private Secret emptyPassword; + // give the secret some data so that it is hidden and not a regular field! + private Secret populatedPassword = Secret.fromString("secret!"); + private String textarea; + private Secret emptySecretTextarea; + private Secret populatedSecretTextarea = Secret.fromString("sensitive!");; + + @DataBoundConstructor + public ValidatingDescribable() { + } + + public Secret getEmptyPassword() { + return emptyPassword; + } + + @DataBoundSetter + public void setEmptyPassword(Secret emptyPassword) { + this.emptyPassword = emptyPassword; + } + + public Secret getPopulatedPassword() { + return populatedPassword; + } + + @DataBoundSetter + public void setPopulatedPassword(Secret populatedPassword) { + this.populatedPassword = populatedPassword; + } + + public String getTextarea() { + return textarea; + } + + @DataBoundSetter + public void setTextarea(String textarea) { + this.textarea = textarea; + } + + public Secret getEmptySecretTextarea() { + return emptySecretTextarea; + } + + @DataBoundSetter + public void setEmptySecretTextarea(Secret emptySecretTextarea) { + this.emptySecretTextarea = emptySecretTextarea; + } + + public Secret getPopulatedSecretTextarea() { + return populatedSecretTextarea; + } + + @DataBoundSetter + public void setPopulatedSecretTextarea(Secret populatedSecretTextarea) { + this.populatedSecretTextarea = populatedSecretTextarea; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + // not used for the test class but useful for interactive debugging to check the validation has been called + AtomicInteger i = new AtomicInteger(); + + @Override + public String getDisplayName() { + return "Validation Testing"; + } + + public FormValidation doCheckEmptyPassword(@QueryParameter String value) { + return FormValidation.ok("FormValidation: Password (empty)" + i.getAndIncrement()); + } + + public FormValidation doCheckPopulatedPassword(@QueryParameter String value) { + return FormValidation.ok("FormValidation: Password (populated)" + i.getAndIncrement()); + } + + public FormValidation doCheckTextarea(@QueryParameter String value) { + return FormValidation.ok("FormValidation: Textarea" + i.getAndIncrement()); + } + + public FormValidation doCheckEmptySecretTextarea(@QueryParameter String value) { + return FormValidation.ok("FormValidation: SecretTextarea (empty)" + i.getAndIncrement()); + } + + public FormValidation doCheckPopulatedSecretTextarea(@QueryParameter String value) { + return FormValidation.ok("FormValidation: SecretTextarea (populated)" + i.getAndIncrement()); + } + + @Override + public boolean isApplicable(Class jobType) { + return true; + } + + } + + } + } diff --git a/test/src/test/java/jenkins/ClassPathTest.java b/test/src/test/java/jenkins/ClassPathTest.java index 01b29253e0cc..4629a8f65faf 100644 --- a/test/src/test/java/jenkins/ClassPathTest.java +++ b/test/src/test/java/jenkins/ClassPathTest.java @@ -26,9 +26,7 @@ import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,7 +44,7 @@ public class ClassPathTest { @Rule public ErrorCollector errors = new ErrorCollector(); - private static final Set KNOWN_VIOLATIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + private static final Set KNOWN_VIOLATIONS = Set.of( // TODO duplicated in [jline-2.14.6.jar, jansi-1.11.jar] "org/fusesource/hawtjni/runtime/Callback.class", "org/fusesource/hawtjni/runtime/JNIEnv.class", @@ -75,7 +73,7 @@ public class ClassPathTest { "org/fusesource/jansi/internal/Kernel32$SMALL_RECT.class", "org/fusesource/jansi/internal/Kernel32.class", "org/fusesource/jansi/internal/WindowsSupport.class", - "org/fusesource/jansi/WindowsAnsiOutputStream.class"))); + "org/fusesource/jansi/WindowsAnsiOutputStream.class"); @Issue("JENKINS-46754") @Test diff --git a/test/src/test/java/jenkins/install/SetupWizardTest.java b/test/src/test/java/jenkins/install/SetupWizardTest.java index 7168f93d93af..299937df462e 100644 --- a/test/src/test/java/jenkins/install/SetupWizardTest.java +++ b/test/src/test/java/jenkins/install/SetupWizardTest.java @@ -50,15 +50,17 @@ import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.Set; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import jenkins.model.Jenkins; import jenkins.util.JSONSignatureValidator; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.Callback; import org.htmlunit.Page; import org.junit.Before; import org.junit.Rule; @@ -336,7 +338,7 @@ protected JSONSignatureValidator getJsonSignatureValidator(String name) { } } - private static class RemoteUpdateSiteHandler extends AbstractHandler { + private static class RemoteUpdateSiteHandler extends Handler.Abstract { private String serverContext; private boolean includeSignature; @@ -347,15 +349,18 @@ private static class RemoteUpdateSiteHandler extends AbstractHandler { } @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - String responseBody = getWebServerResource(target, request.getParameter("version")); + public boolean handle(Request request, Response response, Callback callback) throws IOException { + String target = request.getHttpURI().getPath(); + String version = Request.extractQueryParameters(request).get("version").getValue(); + String responseBody = getWebServerResource(target, version); if (responseBody != null) { - baseRequest.setHandled(true); - response.setContentType("text/plain; charset=utf-8"); - response.setStatus(HttpServletResponse.SC_OK); - response.getOutputStream().write(responseBody.getBytes(StandardCharsets.UTF_8)); + response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8"); + response.setStatus(HttpStatus.OK_200); + Content.Sink.write(response, true, responseBody, callback); + return true; } else { - response.sendError(404); + Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404); + return true; } } diff --git a/test/src/test/java/jenkins/management/AdministrativeMonitorsDecoratorTest.java b/test/src/test/java/jenkins/management/AdministrativeMonitorsDecoratorTest.java index 37d28638e65b..691b9d2c8868 100644 --- a/test/src/test/java/jenkins/management/AdministrativeMonitorsDecoratorTest.java +++ b/test/src/test/java/jenkins/management/AdministrativeMonitorsDecoratorTest.java @@ -24,15 +24,22 @@ package jenkins.management; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import hudson.ExtensionList; import hudson.model.AdministrativeMonitor; import hudson.model.User; +import hudson.security.ACL; +import hudson.security.Permission; import jenkins.model.Jenkins; import org.jenkinsci.Symbol; import org.junit.Rule; import org.junit.Test; +import org.jvnet.hudson.test.FlagRule; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; @@ -43,6 +50,9 @@ public class AdministrativeMonitorsDecoratorTest { @Rule public JenkinsRule j = new JenkinsRule(); + @Rule + public final FlagRule managePermissionRule = FlagRule.systemProperty("jenkins.security.ManagePermission", "true"); + @Test public void ensureAdminMonitorsAreNotRunPerNonAdminPage() throws Exception { j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); @@ -131,4 +141,80 @@ public boolean isSecurity() { return true; } } + + @Test + public void ensureAdminMonitorsCanBeSeenByManagers() { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + var managerLogin = "manager"; + var systemReadLogin = "system-reader"; + var managerUser = User.getById(managerLogin, true); + var systemReadUser = User.getById(systemReadLogin, true); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.MANAGE, Jenkins.READ).everywhere().to(managerLogin) + .grant(Jenkins.READ, Jenkins.SYSTEM_READ).everywhere().to(systemReadLogin) + ); + + try (var ignored = ACL.as2(managerUser.impersonate2())) { + assertThat(Jenkins.get().getActiveAdministrativeMonitors(), hasItem(instanceOf(ManagerAdministrativeMonitor.class))); + } + try (var ignored = ACL.as2(systemReadUser.impersonate2())) { + assertThat(Jenkins.get().getActiveAdministrativeMonitors(), not(hasItem(instanceOf(ManagerAdministrativeMonitor.class)))); + } + } + + @TestExtension("ensureAdminMonitorsCanBeSeenByManagers") + public static class ManagerAdministrativeMonitor extends AdministrativeMonitor { + @Override + public Permission getRequiredPermission() { + return Jenkins.MANAGE; + } + + @Override + public boolean isActivated() { + return true; + } + } + + @Test + public void ensureAdminMonitorsCanBeSeenByManagersOrSystemReaders() { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + var managerLogin = "manager"; + var systemReadLogin = "system-reader"; + var managerUser = User.getById(managerLogin, true); + var systemReadUser = User.getById(systemReadLogin, true); + + j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.MANAGE, Jenkins.READ).everywhere().to(managerLogin) + .grant(Jenkins.READ, Jenkins.SYSTEM_READ).everywhere().to(systemReadLogin) + ); + + try (var ignored = ACL.as2(managerUser.impersonate2())) { + assertThat(Jenkins.get().getActiveAdministrativeMonitors(), hasItem(instanceOf(ManagerOrSystemReaderAdministrativeMonitor.class))); + } + try (var ignored = ACL.as2(systemReadUser.impersonate2())) { + assertThat(Jenkins.get().getActiveAdministrativeMonitors(), hasItem(instanceOf(ManagerOrSystemReaderAdministrativeMonitor.class))); + } + } + + @TestExtension("ensureAdminMonitorsCanBeSeenByManagersOrSystemReaders") + public static class ManagerOrSystemReaderAdministrativeMonitor extends AdministrativeMonitor { + + private static final Permission[] REQUIRED_ANY_PERMISSIONS = {Jenkins.MANAGE, Jenkins.SYSTEM_READ}; + + @Override + public void checkRequiredPermission() { + Jenkins.get().checkAnyPermission(REQUIRED_ANY_PERMISSIONS); + } + + @Override + public boolean hasRequiredPermission() { + return Jenkins.get().hasAnyPermission(REQUIRED_ANY_PERMISSIONS); + } + + @Override + public boolean isActivated() { + return true; + } + } } diff --git a/test/src/test/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfigurationTest.java b/test/src/test/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfigurationTest.java new file mode 100644 index 000000000000..9e7cf9f4ff81 --- /dev/null +++ b/test/src/test/java/jenkins/model/GlobalComputerRetentionCheckIntervalConfigurationTest.java @@ -0,0 +1,146 @@ +package jenkins.model; + +import static org.junit.Assert.assertEquals; + +import hudson.model.Descriptor; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.logging.Level; +import net.sf.json.JSONObject; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.LoggerRule; +import org.kohsuke.stapler.Stapler; + + +/** + * Ensure interval bounds are enforced when re-configuring and loading from disk. Also ensure default value handling. + * + * @author Jakob Ackermann + */ +public class GlobalComputerRetentionCheckIntervalConfigurationTest { + @Rule + public JenkinsRule j = new JenkinsRule(); + @Rule + public LoggerRule logging = new LoggerRule(); + + private File getConfig(GlobalComputerRetentionCheckIntervalConfiguration c) { + return new File(j.jenkins.getRootDir(), c.getId() + ".xml"); + } + + private void recordWarnings() { + logging.record(GlobalComputerRetentionCheckIntervalConfiguration.class, Level.INFO).capture(100); + } + + @Test + public void bootWithMissingCfg() { + recordWarnings(); + GlobalComputerRetentionCheckIntervalConfiguration c = new GlobalComputerRetentionCheckIntervalConfiguration(); + c.load(); + assertEquals("default", 60, c.getComputerRetentionCheckInterval()); + assertEquals("no fallback message", logging.getRecords().size(), 0); + } + + private void writeConfig(GlobalComputerRetentionCheckIntervalConfiguration c, int interval) throws IOException { + String bad = "" + + "\n" + + "\n" + + " " + interval + "\n" + + ""; + Files.writeString(getConfig(c).toPath(), bad, StandardCharsets.UTF_8); + } + + private void checkUsesFallbackAfterLoadOf(int interval) throws IOException { + recordWarnings(); + GlobalComputerRetentionCheckIntervalConfiguration c = new GlobalComputerRetentionCheckIntervalConfiguration(); + writeConfig(c, interval); + c.load(); + assertEquals("uses default", 60, c.getComputerRetentionCheckInterval()); + assertEquals("prints one fallback message", 1, logging.getRecords().size()); + assertEquals("fallback message content", "computerRetentionCheckInterval must be greater than zero, falling back to 60s", logging.getRecords().get(0).getMessage()); + } + + @Test + public void bootWithNegative() throws IOException { + checkUsesFallbackAfterLoadOf(-1); + } + + @Test + public void bootWithZero() throws IOException { + checkUsesFallbackAfterLoadOf(0); + } + + @Test + public void bootWithPositive() throws IOException { + recordWarnings(); + GlobalComputerRetentionCheckIntervalConfiguration c = new GlobalComputerRetentionCheckIntervalConfiguration(); + writeConfig(c, 1); + c.load(); + assertEquals("uses custom value", 1, c.getComputerRetentionCheckInterval()); + assertEquals("no fallback message", 0, logging.getRecords().size()); + } + + @Test + public void bootWithTooLargeValue() throws IOException { + recordWarnings(); + GlobalComputerRetentionCheckIntervalConfiguration c = new GlobalComputerRetentionCheckIntervalConfiguration(); + writeConfig(c, 1337); + c.load(); + assertEquals("uses default", 60, c.getComputerRetentionCheckInterval()); + assertEquals("prints one fallback message", 1, logging.getRecords().size()); + assertEquals("fallback message content", "computerRetentionCheckInterval is limited to 60s", logging.getRecords().get(0).getMessage()); + } + + @Test + public void saveCycle() { + recordWarnings(); + GlobalComputerRetentionCheckIntervalConfiguration c = new GlobalComputerRetentionCheckIntervalConfiguration(); + + JSONObject json = new JSONObject(); + json.element("computerRetentionCheckInterval", 5); + try { + c.configure(Stapler.getCurrentRequest(), json); + } catch (Descriptor.FormException e) { + throw new RuntimeException(e); + } + assertEquals("stores value", 5, c.getComputerRetentionCheckInterval()); + + GlobalComputerRetentionCheckIntervalConfiguration c2 = new GlobalComputerRetentionCheckIntervalConfiguration(); + c2.load(); + assertEquals("round trip value", 5, c2.getComputerRetentionCheckInterval()); + assertEquals("no fallback message", 0, logging.getRecords().size()); + } + + private void checkSaveInvalidValueOf(int interval, String message) { + recordWarnings(); + GlobalComputerRetentionCheckIntervalConfiguration c = new GlobalComputerRetentionCheckIntervalConfiguration(); + + JSONObject json = new JSONObject(); + json.element("computerRetentionCheckInterval", interval); + try { + c.configure(Stapler.getCurrentRequest(), json); + throw new RuntimeException("expected .configure() to throw"); + } catch (Descriptor.FormException e) { + assertEquals(e.getMessage(), message); + } + assertEquals("does not store value", 60, c.getComputerRetentionCheckInterval()); + + GlobalComputerRetentionCheckIntervalConfiguration c2 = new GlobalComputerRetentionCheckIntervalConfiguration(); + c2.load(); + assertEquals("does not persist value", 60, c2.getComputerRetentionCheckInterval()); + assertEquals("no fallback message", 0, logging.getRecords().size()); + } + + @Test + public void saveInvalidValueTooLow() { + checkSaveInvalidValueOf(0, "java.lang.IllegalArgumentException: interval must be greater than zero"); + } + + @Test + public void saveInvalidValueTooHigh() { + checkSaveInvalidValueOf(1337, "java.lang.IllegalArgumentException: interval must be below or equal 60s"); + } +} diff --git a/test/src/test/java/jenkins/model/RunIdMigratorTest.java b/test/src/test/java/jenkins/model/RunIdMigratorTest.java index 4ade1afc84e1..cb7903435d80 100644 --- a/test/src/test/java/jenkins/model/RunIdMigratorTest.java +++ b/test/src/test/java/jenkins/model/RunIdMigratorTest.java @@ -39,7 +39,6 @@ import java.nio.charset.StandardCharsets; import org.htmlunit.HttpMethod; import org.htmlunit.WebRequest; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -58,7 +57,6 @@ public void legacyIdsPresent() throws Exception { assertTrue(legacyIds.exists()); } - @Ignore("TODO Item#onCreatedFromScratch is not called") @Issue("JENKINS-64356") @Test public void legacyIdsPresentViaRestApi() throws Exception { @@ -82,7 +80,6 @@ public void legacyIdsPresentViaRestApi() throws Exception { assertTrue(legacyIds.exists()); } - @Ignore("TODO Item#onCreatedFromScratch is not called") @Issue("JENKINS-64356") @Test public void legacyIdsPresentViaCli() { diff --git a/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java b/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java index 59f89e8f4977..c746fc7cc4d5 100644 --- a/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java +++ b/test/src/test/java/jenkins/security/ApiCrumbExclusionTest.java @@ -108,7 +108,7 @@ private void makeRequestAndFail(int expectedCode) { } private void checkWeCanChangeMyDescription(int expectedCode) throws IOException, SAXException { - HtmlPage page = wc.goTo("me/configure"); + HtmlPage page = wc.goTo("me/account/"); HtmlForm form = page.getFormByName("config"); form.getTextAreaByName("_.description").setText("random description: " + Math.random()); diff --git a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java index 30361ed8658c..162ac231b664 100644 --- a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java +++ b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java @@ -80,7 +80,7 @@ public void basics() throws Exception { assertEquals(u, wc.executeOnServer(User::current)); // Make sure the UI shows the token to the user - HtmlPage config = wc.goTo(u.getUrl() + "/configure"); + HtmlPage config = wc.goTo(u.getUrl() + "/security/"); HtmlForm form = config.getFormByName("config"); assertEquals(token, form.getInputByName("_.apiToken").getValue()); @@ -126,7 +126,7 @@ public void adminsShouldBeUnableToSeeTokensByDefault() throws Exception { // Make sure the UI does not show the token to another user WebClient wc = createClientForUser("bar"); - HtmlPage config = wc.goTo(u.getUrl() + "/configure"); + HtmlPage config = wc.goTo(u.getUrl() + "/security/"); HtmlForm form = config.getFormByName("config"); assertEquals(Messages.ApiTokenProperty_ChangeToken_TokenIsHidden(), form.getInputByName("_.apiToken").getValue()); } diff --git a/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest.java b/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest.java index 873194dfb089..940715b0fe6a 100644 --- a/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest.java +++ b/test/src/test/java/jenkins/security/BasicHeaderApiTokenAuthenticatorTest.java @@ -70,7 +70,7 @@ public void legacyToken_regularCase() throws Throwable { // default SecurityListener will save the user when adding the LastGrantedAuthoritiesProperty // and so the user is persisted wc.login("user1"); - HtmlPage page = wc.goTo("user/user1/configure"); + HtmlPage page = wc.goTo("user/user1/security/"); String tokenValue = ((HtmlTextInput) page.getDocumentElement().querySelector("#apiToken")).getText(); token.set(tokenValue); } @@ -118,7 +118,7 @@ public void legacyToken_withoutLastGrantedAuthorities() throws Throwable { { JenkinsRule.WebClient wc = j.createWebClient(); wc.login("user1"); - HtmlPage page = wc.goTo("user/user1/configure"); + HtmlPage page = wc.goTo("user/user1/security/"); String tokenValue = ((HtmlTextInput) page.getDocumentElement().querySelector("#apiToken")).getText(); token.set(tokenValue); } diff --git a/test/src/test/java/jenkins/security/LastGrantedAuthoritiesPropertyTest.java b/test/src/test/java/jenkins/security/LastGrantedAuthoritiesPropertyTest.java index 4d29e609860f..5c1c7318c3a9 100644 --- a/test/src/test/java/jenkins/security/LastGrantedAuthoritiesPropertyTest.java +++ b/test/src/test/java/jenkins/security/LastGrantedAuthoritiesPropertyTest.java @@ -43,7 +43,7 @@ public void basicFlow() throws Exception { assertAuthorities(u.impersonate2(), "alice:authenticated:development:us"); // visiting the configuration page shouldn't change authorities - HtmlPage pg = wc.goTo("user/alice/configure"); + HtmlPage pg = wc.goTo("user/alice/account/"); j.submit(pg.getFormByName("config")); p = u.getProperty(LastGrantedAuthoritiesProperty.class); diff --git a/test/src/test/java/jenkins/security/RekeySecretAdminMonitorTest.java b/test/src/test/java/jenkins/security/RekeySecretAdminMonitorTest.java deleted file mode 100644 index c04c2a1c893a..000000000000 --- a/test/src/test/java/jenkins/security/RekeySecretAdminMonitorTest.java +++ /dev/null @@ -1,189 +0,0 @@ -package jenkins.security; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import hudson.ExtensionList; -import hudson.FilePath; -import hudson.Util; -import hudson.util.Secret; -import hudson.util.SecretHelper; -import java.io.File; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; -import java.util.regex.Pattern; -import java.util.stream.Stream; -import javax.crypto.Cipher; -import org.hamcrest.MatcherAssert; -import org.hamcrest.Matchers; -import org.htmlunit.ElementNotFoundException; -import org.htmlunit.html.DomNodeUtil; -import org.htmlunit.html.HtmlButton; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlPage; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRecipe; -import org.jvnet.hudson.test.JenkinsRule; -import org.xml.sax.SAXException; - -/** - * @author Kohsuke Kawaguchi - */ -public class RekeySecretAdminMonitorTest { - - @Rule public JenkinsRule j = new JenkinsRule(); - - RekeySecretAdminMonitor monitor; - - final String plain_regex_match = ".*\\{[A-Za-z0-9+/]+={0,2}}.*"; - - @Before - public void setUp() { - monitor = ExtensionList.lookupSingleton(RekeySecretAdminMonitor.class); - } - - @JenkinsRecipe(RekeySecretAdminMonitorTest.WithTestSecret.RuleRunnerImpl.class) - @Target(METHOD) - @Retention(RUNTIME) - public @interface WithTestSecret { - class RuleRunnerImpl extends JenkinsRecipe.Runner { - @Override - public void setup(JenkinsRule jenkinsRule, WithTestSecret recipe) { - SecretHelper.set(TEST_KEY); - } - - @Override - public void tearDown(JenkinsRule jenkinsRule, WithTestSecret recipe) { - SecretHelper.set(null); - } - } - } - - @JenkinsRecipe(RekeySecretAdminMonitorTest.WithScanOnBoot.RuleRunnerImpl.class) - @Target(METHOD) - @Retention(RUNTIME) - public @interface WithScanOnBoot { - class RuleRunnerImpl extends JenkinsRecipe.Runner { - @Override - public void decorateHome(JenkinsRule jenkinsRule, File home) throws Exception { - // schedule a scan on boot - File f = new File(home, RekeySecretAdminMonitor.class.getName() + "/scanOnBoot"); - f.getParentFile().mkdirs(); - new FilePath(f).touch(0); - - // and stage some data - putSomeOldData(home); - } - } - } - - private static void putSomeOldData(File dir) throws Exception { - File xml = new File(dir, "foo.xml"); - Files.writeString(xml.toPath(), "" + encryptOld(TEST_KEY) + "", StandardCharsets.UTF_8); - } - - private void verifyRewrite(File dir) throws Exception { - Path xml = dir.toPath().resolve("foo.xml"); - Pattern pattern = Pattern.compile("" + plain_regex_match + ""); - MatcherAssert.assertThat(Files.readString(xml, StandardCharsets.UTF_8).trim(), Matchers.matchesRegex(pattern)); - } - - @WithTestSecret - @Test - public void testBasicWorkflow() throws Exception { - putSomeOldData(j.jenkins.getRootDir()); - monitor.setNeeded(); - - JenkinsRule.WebClient wc = j.createWebClient(); - - // one should see the warning. try scheduling it - assertFalse(monitor.isScanOnBoot()); - HtmlForm form = getRekeyForm(wc); - j.submit(form, "schedule"); - assertTrue(monitor.isScanOnBoot()); - form = getRekeyForm(wc); - assertTrue(getButton(form, 1).isDisabled()); - - // run it now - assertFalse(monitor.getLogFile().exists()); - j.submit(form, "background"); - assertTrue(monitor.getLogFile().exists()); - - // should be no warning/error now - HtmlPage manage = wc.goTo("manage"); - assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='error']").size()); - assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='warning']").size()); - - // and the data should be rewritten - verifyRewrite(j.jenkins.getRootDir()); - assertTrue(monitor.isDone()); - - // dismiss and the message will be gone - assertTrue(monitor.isEnabled()); - form = getRekeyForm(wc); - j.submit(form, "dismiss"); - assertFalse(monitor.isEnabled()); - assertThrows(ElementNotFoundException.class, () -> getRekeyForm(wc)); - } - - private HtmlForm getRekeyForm(JenkinsRule.WebClient wc) throws IOException, SAXException { - return wc.goTo("manage").getFormByName("rekey"); - } - - private HtmlButton getButton(HtmlForm form, int index) { - // due to the removal of method HtmlElement.getHtmlElementsByTagName - Stream buttonStream = form.getElementsByTagName("button").stream() - .filter(HtmlButton.class::isInstance) - .map(HtmlButton.class::cast); - - if (index > 0) { - buttonStream = buttonStream.skip(index); - } - - return buttonStream - .findFirst() - .orElse(null); - } - - @WithTestSecret - @WithScanOnBoot - @Test - public void testScanOnBoot() throws Exception { - JenkinsRule.WebClient wc = j.createWebClient(); - - // scan on boot should have run the scan - assertTrue(monitor.getLogFile().exists()); - assertFalse("scan on boot should have turned this off", monitor.isScanOnBoot()); - - // and data should be migrated - verifyRewrite(j.jenkins.getRootDir()); - - // should be no warning/error now - HtmlPage manage = wc.goTo("manage"); - assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='error']").size()); - assertEquals(0, DomNodeUtil.selectNodes(manage, "//*[class='warning']").size()); - } - - private static String encryptOld(String str) throws Exception { - Cipher cipher = Secret.getCipher("AES"); - cipher.init(Cipher.ENCRYPT_MODE, Util.toAes128Key(TEST_KEY)); - return Base64.getEncoder().encodeToString(cipher.doFinal((str + "::::MAGIC::::").getBytes(StandardCharsets.UTF_8))); - } - - private String encryptNew(String str) { - return Secret.fromString(str).getEncryptedValue(); - } - - private static final String TEST_KEY = "superDuperSecretWasNotSoSecretAfterAll"; -} diff --git a/test/src/test/java/jenkins/security/Security2780Test.java b/test/src/test/java/jenkins/security/Security2780Test.java index 0d876daa7328..7b471537f38b 100644 --- a/test/src/test/java/jenkins/security/Security2780Test.java +++ b/test/src/test/java/jenkins/security/Security2780Test.java @@ -27,7 +27,7 @@ public void buildButtonTooltipHasNoXss() throws Exception { AtomicBoolean alertTriggered = new AtomicBoolean(false); wc.setAlertHandler((p, s) -> alertTriggered.set(true)); HtmlPage page = wc.goTo(""); - page.executeJavaScript("document.querySelector('a.jenkins-table__button')._tippy.show()"); + page.executeJavaScript("document.querySelector('.jenkins-table a.jenkins-button')._tippy.show()"); wc.waitForBackgroundJavaScript(2000L); ScriptResult result = page.executeJavaScript("document.querySelector('.tippy-content').innerHTML;"); Object jsResult = result.getJavaScriptResult(); diff --git a/test/src/test/java/jenkins/security/Security3030Test.java b/test/src/test/java/jenkins/security/Security3030Test.java index e2d1e4bf5bf7..5e712bdc4e31 100644 --- a/test/src/test/java/jenkins/security/Security3030Test.java +++ b/test/src/test/java/jenkins/security/Security3030Test.java @@ -43,9 +43,10 @@ import java.nio.charset.StandardCharsets; import java.util.Random; import javax.servlet.ServletException; -import org.apache.commons.fileupload.FileCountLimitExceededException; -import org.apache.commons.fileupload.FileUploadBase; -import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadSizeException; import org.htmlunit.HttpMethod; import org.htmlunit.WebRequest; import org.junit.Assert; @@ -71,18 +72,18 @@ public void fewFilesStapler() throws IOException { @Test public void tooManyFilesStapler() throws Exception { - ServletException ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 10, 1000, 20, FileCountLimitExceededException.class); + ServletException ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 10, 1000, 20, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_FILES")); - ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1000, 10, 10, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1000, 10, 10, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_FILES")); try (FieldValue v = withStaticField(RequestImpl.class, "FILEUPLOAD_MAX_FILES", 10_000)) { assertSubmissionOK(StaplerRequestFormAction.instance(), 1000, 10, 10); - ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 10_000, 10, 10, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 10_000, 10, 10, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_FILES")); } - ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 10, 1000, 20, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 10, 1000, 20, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_FILES")); - ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1000, 10, 10, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1000, 10, 10, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_FILES")); } @@ -91,7 +92,7 @@ public void tooLargeFilesStapler() throws Exception { assertSubmissionOK(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024); try (FieldValue v = withStaticField(RequestImpl.class, "FILEUPLOAD_MAX_FILE_SIZE", 1024 * 1024)) { assertSubmissionOK(StaplerRequestFormAction.instance(), 200, 100, 1024); - ServletException ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadBase.FileSizeLimitExceededException.class); + ServletException ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadByteCountLimitException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_FILE_SIZE")); } assertSubmissionOK(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024); @@ -102,7 +103,7 @@ public void tooLargeSubmissionStapler() throws Exception { assertSubmissionOK(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024); try (FieldValue v = withStaticField(RequestImpl.class, "FILEUPLOAD_MAX_SIZE", 1024 * 1024)) { assertSubmissionOK(StaplerRequestFormAction.instance(), 200, 100, 1024); - ServletException ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadBase.SizeLimitExceededException.class); + ServletException ex = assertSubmissionThrows(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadSizeException.class); assertThat(ex.getMessage(), containsString(RequestImpl.class.getName() + ".FILEUPLOAD_MAX_SIZE")); } assertSubmissionOK(StaplerRequestFormAction.instance(), 1, 50, 10 * 1024 * 1024); @@ -116,18 +117,18 @@ public void fewFilesParser() throws IOException { @Test public void tooManyFilesParser() throws Exception { - ServletException ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 10, 1000, 20, FileCountLimitExceededException.class); + ServletException ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 10, 1000, 20, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES")); - ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1000, 10, 10, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1000, 10, 10, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES")); try (FieldValue v = withStaticField(MultipartFormDataParser.class, "FILEUPLOAD_MAX_FILES", 10_000)) { assertSubmissionOK(MultipartFormDataParserAction.instance(), 1000, 10, 10); - ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 10_000, 10, 10, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 10_000, 10, 10, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES")); } - ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 10, 1000, 20, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 10, 1000, 20, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES")); - ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1000, 10, 10, FileCountLimitExceededException.class); + ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1000, 10, 10, FileUploadFileCountLimitException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILES")); } @@ -136,7 +137,7 @@ public void tooLargeFilesParser() throws Exception { assertSubmissionOK(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024); try (FieldValue v = withStaticField(MultipartFormDataParser.class, "FILEUPLOAD_MAX_FILE_SIZE", 1024 * 1024)) { assertSubmissionOK(MultipartFormDataParserAction.instance(), 200, 100, 1024); - ServletException ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadBase.FileSizeLimitExceededException.class); + ServletException ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadByteCountLimitException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_FILE_SIZE")); } assertSubmissionOK(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024); @@ -147,7 +148,7 @@ public void tooLargeSubmissionParser() throws Exception { assertSubmissionOK(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024); try (FieldValue v = withStaticField(MultipartFormDataParser.class, "FILEUPLOAD_MAX_SIZE", 1024 * 1024)) { assertSubmissionOK(MultipartFormDataParserAction.instance(), 200, 100, 1024); - ServletException ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadBase.SizeLimitExceededException.class); + ServletException ex = assertSubmissionThrows(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024, FileUploadSizeException.class); assertThat(ex.getMessage(), containsString(MultipartFormDataParser.class.getName() + ".FILEUPLOAD_MAX_SIZE")); } assertSubmissionOK(MultipartFormDataParserAction.instance(), 1, 50, 10 * 1024 * 1024); @@ -271,6 +272,14 @@ public HttpResponse doSubmitMultipart(StaplerRequest req) throws FileUploadExcep return processMultipartAndUnwrap(req); } else { actualWrapped = Assert.assertThrows(expectedWrapped, () -> processMultipartAndUnwrap(req)); + + // The client might still be sending us more of the request, but we have had enough of it already and + // have decided to stop processing it. Drain the read end of the socket so that the client can finish + // sending its request in order to read the response we are about to provide. + try (OutputStream os = OutputStream.nullOutputStream()) { + req.getInputStream().transferTo(os); + } + return HttpResponses.ok(); } } @@ -311,7 +320,7 @@ public static StaplerRequestFormAction instance() { } protected HttpResponse processMultipart(StaplerRequest req) throws ServletException, IOException { - req.getFileItem("any-name"); + req.getFileItem2("any-name"); return HttpResponses.ok(); } } diff --git a/test/src/test/java/jenkins/security/Security3245Test.java b/test/src/test/java/jenkins/security/Security3245Test.java index 9c9a37b46581..2202632b26e6 100644 --- a/test/src/test/java/jenkins/security/Security3245Test.java +++ b/test/src/test/java/jenkins/security/Security3245Test.java @@ -30,9 +30,9 @@ public class Security3245Test { @Issue("SECURITY-3245") @Test - public void captionCannotAttributeEscape() throws Exception { + public void captionCannotElementEscape() throws Exception { FreeStyleProject p = j.createFreeStyleProject("p"); - p.getBuildersList().add(new ExpandableDetailsNoteTestAction("' onclick=alert(1) foo='bar", "

")); + p.getBuildersList().add(new ExpandableDetailsNoteTestAction("", "

")); FreeStyleBuild build = j.buildAndAssertSuccess(p); AtomicBoolean alerts = new AtomicBoolean(); @@ -40,12 +40,9 @@ public void captionCannotAttributeEscape() throws Exception { wc.setAlertHandler((pr, s) -> alerts.set(true)); final HtmlPage page = wc.goTo(build.getUrl() + "console"); String content = page.getWebResponse().getContentAsString(); - assertThat(content, containsString("")); - - // Execute JavaScript code to simulate click event - String jsCode = "document.querySelector('.reveal-expandable-detail').dispatchEvent(new MouseEvent('click'));"; - page.executeJavaScript(jsCode); - + assertThat(content, containsString("")); + // check that alert was not executed Assert.assertFalse("Alert not expected", alerts.get()); } } diff --git a/test/src/test/java/jenkins/security/Security3430Test.java b/test/src/test/java/jenkins/security/Security3430Test.java new file mode 100644 index 000000000000..f39702fd4204 --- /dev/null +++ b/test/src/test/java/jenkins/security/Security3430Test.java @@ -0,0 +1,295 @@ +package jenkins.security; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import hudson.ExtensionList; +import hudson.model.Computer; +import hudson.remoting.Channel; +import hudson.remoting.Launcher; +import hudson.slaves.SlaveComputer; +import hudson.util.RingBufferLogHandler; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import jenkins.bouncycastle.api.InstallBouncyCastleJCAProvider; +import jenkins.security.s2m.JarURLValidatorImpl; +import jenkins.slaves.RemotingVersionInfo; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.InboundAgentRule; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RealJenkinsRule; +import org.kohsuke.args4j.Argument; +import org.kohsuke.stapler.Stapler; + +public class Security3430Test { + @Rule + public RealJenkinsRule jj = new RealJenkinsRule().withLogger(JarURLValidatorImpl.class, Level.FINEST); + + @Rule + public InboundAgentRule agents = new InboundAgentRule(); + + @Test + public void runWithOldestSupportedAgentJar() throws Throwable { + runWithRemoting(RemotingVersionInfo.getMinimumSupportedVersion().toString(), "/old-remoting/remoting-minimum-supported.jar", true); + } + + @Test + public void runWithPreviousAgentJar() throws Throwable { + runWithRemoting("3256.v88a_f6e922152", "/old-remoting/remoting-before-SECURITY-3430-fix.jar", true); + } + + @Test + public void runWithCurrentAgentJar() throws Throwable { + runWithRemoting(null, null, false); + } + + private void runWithRemoting(String expectedRemotingVersion, String remotingResourcePath, boolean requestingJarFromAgent) throws Throwable { + if (expectedRemotingVersion != null) { + FileUtils.copyURLToFile(Security3430Test.class.getResource(remotingResourcePath), new File(jj.getHome(), "agent.jar")); + } + + jj.startJenkins(); + final String agentName = "agent1"; + try { + agents.createAgent(jj, InboundAgentRule.Options.newBuilder().name(agentName).build()); + jj.runRemotely(Security3430Test::_run, agentName, expectedRemotingVersion, requestingJarFromAgent, true); + } finally { + agents.stop(jj, agentName); + } + jj.runRemotely(Security3430Test::disableJarURLValidatorImpl); + final String agentName2 = "agent2"; + try { + agents.createAgent(jj, InboundAgentRule.Options.newBuilder().name(agentName2).build()); + jj.runRemotely(Security3430Test::_run, agentName2, expectedRemotingVersion, requestingJarFromAgent, false); + } finally { + agents.stop(jj, agentName2); + } + } + + // This is quite artificial, but demonstrating that without JarURLValidatorImpl we do not allow any calls from the agent: + private static void disableJarURLValidatorImpl(JenkinsRule j) { + assertTrue(ExtensionList.lookup(ChannelConfigurator.class).remove(ExtensionList.lookupSingleton(JarURLValidatorImpl.class))); + } + + /** + * + * @param agentName the name of the agent we're working with + * @param expectedRemotingVersion The version expected for remoting, or {@code null} if we're using whatever is bundled with this Jenkins. + * @param requestingJarFromAgent {@code true} if and only if we expect to go through {@code ClassLoaderProxy#fetchJar} + * @param hasJenkinsJarURLValidator {@code true} if and only we do not expect {@link jenkins.security.s2m.JarURLValidatorImpl} to be present. Only relevant when {@code requestingJarFromAgent} is {@code true}. + */ + private static void _run(JenkinsRule j, String agentName, String expectedRemotingVersion, Boolean requestingJarFromAgent, Boolean hasJenkinsJarURLValidator) throws Throwable { + final RingBufferLogHandler logHandler = new RingBufferLogHandler(50); + Logger.getLogger(JarURLValidatorImpl.class.getName()).addHandler(logHandler); + final List logRecords = logHandler.getView(); + + final Computer computer = j.jenkins.getComputer(agentName); + assertThat(computer, instanceOf(SlaveComputer.class)); + SlaveComputer agent = (SlaveComputer) computer; + final Channel channel = agent.getChannel(); + if (expectedRemotingVersion != null) { + final String result = channel.call(new AgentVersionCallable()); + assertThat(result, is(expectedRemotingVersion)); + } + + logHandler.clear(); + + { // regular behavior + if (hasJenkinsJarURLValidator) { + // it works + assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class)); + if (requestingJarFromAgent) { + assertThat(logRecords, hasItem(logMessageContainsString("Allowing URL: file:/"))); + } else { + assertThat(logRecords, is(empty())); + } + + logHandler.clear(); + assertFalse(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class)); + assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL")))); + assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL")))); + } else { + // outdated remoting.jar will fail, but up to date one passes + if (requestingJarFromAgent) { + final IOException ex = assertThrows(IOException.class, () -> channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class)); + assertThat(ex.getMessage(), containsString("No hudson.remoting.JarURLValidator has been set for this channel, so all #fetchJar calls are rejected. This is likely a bug in Jenkins. As a workaround, try updating the agent.jar file.")); + } else { + assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class)); + assertThat(logRecords, is(empty())); + } + } + } + + logHandler.clear(); + + if (hasJenkinsJarURLValidator) { // Start rejecting everything; only applies to JarURLValidatorImpl + System.setProperty(JarURLValidatorImpl.class.getName() + ".REJECT_ALL", "true"); + + // Identify that a jar was already loaded: + assertFalse(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class)); + assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL")))); + assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL")))); + + logHandler.clear(); + + // different jar file than before, old remoting will fail due to call through ClassLoaderProxy#fetchJar, new remoting passes + if (requestingJarFromAgent) { + final IOException ioException = assertThrows(IOException.class, () -> channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Argument.class)); + assertThat(ioException.getMessage(), containsString("all attempts by agents to load jars from the controller are rejected")); + assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL")))); + assertThat(logRecords, hasItem(logMessageContainsString("Rejecting URL due to configuration: "))); + } else { + assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, org.kohsuke.args4j.Argument.class)); + assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL")))); + assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL")))); + } + } + + logHandler.clear(); + + if (hasJenkinsJarURLValidator) { // Disable block, only applies to JarURLValidatorImpl + System.clearProperty(JarURLValidatorImpl.class.getName() + ".REJECT_ALL"); + if (requestingJarFromAgent) { + // now it works again for old remoting: + assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, org.kohsuke.args4j.Argument.class)); + assertThat(logRecords, hasItem(logMessageContainsString("Allowing URL: file:/"))); + } else { + // new remoting already has it. + assertFalse(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, org.kohsuke.args4j.Argument.class)); + assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL")))); + assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL")))); + } + assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL due to configuration: ")))); + } + + logHandler.clear(); + + if (hasJenkinsJarURLValidator || !requestingJarFromAgent) { // prepare bouncycastle-api + assertTrue(j.jenkins.getPluginManager().getPlugin("bouncycastle-api").isActive()); + InstallBouncyCastleJCAProvider.on(channel); + channel.call(new ConfirmBouncyCastleLibrary()); + } + + logHandler.clear(); + + { // Exploitation tests + final URL secretKeyFile = new File(j.jenkins.getRootDir(), "secret.key").toURI().toURL(); + final String expectedContent = IOUtils.toString(secretKeyFile, StandardCharsets.UTF_8); + { // Protection is effective when agents request non-jar files: + final InvocationTargetException itex = assertThrows(InvocationTargetException.class, () -> channel.call(new Exploit(secretKeyFile, expectedContent))); + assertThat(itex.getCause(), instanceOf(IOException.class)); + if (hasJenkinsJarURLValidator) { + assertThat(itex.getCause().getMessage(), containsString("This URL does not point to a jar file allowed to be requested by agents")); + assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL")))); + assertThat(logRecords, hasItem(logMessageContainsString("Rejecting URL: "))); + } else { + assertThat(itex.getCause().getMessage(), containsString("No hudson.remoting.JarURLValidator has been set for this channel, so all #fetchJar calls are rejected. This is likely a bug in Jenkins. As a workaround, try updating the agent.jar file.")); + } + } + + logHandler.clear(); + + { // Disable protection and non-jar files can be accessed: + System.setProperty(Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR", "true"); + channel.call(new Exploit(secretKeyFile, expectedContent)); + if (hasJenkinsJarURLValidator) { + assertThat(logRecords, hasItem(logMessageContainsString("Allowing URL due to configuration"))); + assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL")))); + } + System.clearProperty(Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR"); + } + } + } + + private static class AgentVersionCallable extends MasterToSlaveCallable { + @Override + public String call() throws Exception { + return Launcher.VERSION; + } + } + + private static class ConfirmBouncyCastleLibrary extends MasterToSlaveCallable { + @Override + public Void call() throws Exception { + assertNotNull(Security.getProvider("BC")); + return null; + } + } + + private static class Exploit extends MasterToSlaveCallable { + private final URL controllerFilePath; + private final String expectedContent; + + public Exploit(URL controllerFilePath, String expectedContent) { + this.controllerFilePath = controllerFilePath; + this.expectedContent = expectedContent; + } + @Override + public Void call() throws Exception { + final ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + final Field classLoaderProxyField = ccl.getClass().getDeclaredField("proxy"); + classLoaderProxyField.setAccessible(true); + final Object theProxy = classLoaderProxyField.get(ccl); + final Method fetchJarMethod = theProxy.getClass().getDeclaredMethod("fetchJar", URL.class); + fetchJarMethod.setAccessible(true); + final byte[] fetchJarResponse = (byte[]) fetchJarMethod.invoke(theProxy, controllerFilePath); + assertThat(new String(fetchJarResponse, StandardCharsets.UTF_8), is(expectedContent)); + return null; + } + } + + // Would be nice if LoggerRule#recorded equivalents existed for use without LoggerRule, meanwhile: + private static Matcher logMessageContainsString(String needle) { + return new LogMessageContainsString(containsString(needle)); + } + + private static final class LogMessageContainsString extends TypeSafeMatcher { + private final Matcher stringMatcher; + + public LogMessageContainsString(Matcher stringMatcher) { + this.stringMatcher = stringMatcher; + } + + @Override + protected boolean matchesSafely(LogRecord item) { + return stringMatcher.matches(item.getMessage()); + } + + @Override + public void describeTo(Description description) { + description.appendText("a LogRecord with a message matching "); + stringMatcher.describeTo(description); + } + + @Override + protected void describeMismatchSafely(LogRecord item, Description mismatchDescription) { + mismatchDescription.appendText("a LogRecord with the message: "); + mismatchDescription.appendText(item.getMessage()); + } + } +} diff --git a/test/src/test/java/jenkins/security/SecurityContextExecutorServiceTest.java b/test/src/test/java/jenkins/security/SecurityContextExecutorServiceTest.java index 76f64ab67096..ac9df83a2f55 100644 --- a/test/src/test/java/jenkins/security/SecurityContextExecutorServiceTest.java +++ b/test/src/test/java/jenkins/security/SecurityContextExecutorServiceTest.java @@ -106,7 +106,7 @@ public void testRunnableAgainstAllContexts() throws Exception { @Test @PresetData(PresetData.DataSet.NO_ANONYMOUS_READACCESS) public void testCallableAgainstAllContexts() throws Exception { - Callable c = () -> SecurityContextHolder.getContext(); + Callable c = SecurityContextHolder::getContext; SecurityContextHolder.setContext(systemContext); Future result = wrappedService.submit(c); // Assert the context inside the callable thread was set to ACL.SYSTEM2 @@ -127,7 +127,7 @@ public void testCallableAgainstAllContexts() throws Exception { @PresetData(PresetData.DataSet.NO_ANONYMOUS_READACCESS) public void testCallableCollectionAgainstAllContexts() throws Exception { Collection> callables = new ArrayList<>(); - Callable c = () -> SecurityContextHolder.getContext(); + Callable c = SecurityContextHolder::getContext; callables.add(c); callables.add(c); callables.add(c); diff --git a/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsRestartTest.java b/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsRestartTest.java index ea1dec6081ae..d76d382e10c1 100644 --- a/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsRestartTest.java +++ b/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsRestartTest.java @@ -103,7 +103,7 @@ public void roundtripWithRestart() throws Throwable { WebClient restWc = j.createWebClient().withBasicCredentials(u.getId(), tokenValue.get()); checkUserIsConnected(restWc, u.getId()); - HtmlPage config = wc.goTo(u.getUrl() + "/configure"); + HtmlPage config = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, config.getWebResponse().getStatusCode()); assertThat(config.getWebResponse().getContentAsString(), containsString(tokenUuid.get())); assertThat(config.getWebResponse().getContentAsString(), containsString(tokenName)); @@ -113,7 +113,7 @@ public void roundtripWithRestart() throws Throwable { restWc.goToXml("whoAmI/api/xml"); } - HtmlPage configWithStats = wc.goTo(u.getUrl() + "/configure"); + HtmlPage configWithStats = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, configWithStats.getWebResponse().getStatusCode()); HtmlSpan useCounterSpan = configWithStats.getDocumentElement().getOneHtmlElementByAttribute("span", "class", "token-use-counter"); assertThat(useCounterSpan.getTextContent(), containsString("" + NUM_CALL_WITH_TOKEN)); @@ -131,7 +131,7 @@ public void roundtripWithRestart() throws Throwable { WebClient wc = j.createWebClient().login(u.getId()); checkUserIsConnected(wc, u.getId()); - HtmlPage config = wc.goTo(u.getUrl() + "/configure"); + HtmlPage config = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, config.getWebResponse().getStatusCode()); assertThat(config.getWebResponse().getContentAsString(), containsString(tokenUuid.get())); assertThat(config.getWebResponse().getContentAsString(), containsString(TOKEN_NAME)); @@ -144,7 +144,7 @@ public void roundtripWithRestart() throws Throwable { WebClient restWc = j.createWebClient().withBasicCredentials(u.getId(), tokenValue.get()); checkUserIsNotConnected(restWc); - HtmlPage configWithoutToken = wc.goTo(u.getUrl() + "/configure"); + HtmlPage configWithoutToken = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, configWithoutToken.getWebResponse().getStatusCode()); assertThat(configWithoutToken.getWebResponse().getContentAsString(), not(containsString(tokenUuid.get()))); assertThat(configWithoutToken.getWebResponse().getContentAsString(), not(containsString(TOKEN_NAME))); diff --git a/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsTest.java b/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsTest.java index 68185e6efac3..0eb010e77d77 100644 --- a/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsTest.java +++ b/test/src/test/java/jenkins/security/apitoken/ApiTokenStatsTest.java @@ -91,7 +91,7 @@ public void roundtrip() throws Exception { WebClient restWc = j.createWebClient().withBasicCredentials(u.getId(), tokenValue); checkUserIsConnected(restWc, u.getId()); - HtmlPage config = wc.goTo(u.getUrl() + "/configure"); + HtmlPage config = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, config.getWebResponse().getStatusCode()); assertThat(config.getWebResponse().getContentAsString(), containsString(tokenUuid)); assertThat(config.getWebResponse().getContentAsString(), containsString(tokenName)); @@ -102,7 +102,7 @@ public void roundtrip() throws Exception { restWc.goToXml("whoAmI/api/xml"); } - HtmlPage configWithStats = wc.goTo(u.getUrl() + "/configure"); + HtmlPage configWithStats = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, configWithStats.getWebResponse().getStatusCode()); HtmlSpan useCounterSpan = configWithStats.getDocumentElement().getOneHtmlElementByAttribute("span", "class", "token-use-counter"); assertThat(useCounterSpan.getTextContent(), containsString("" + NUM_CALL_WITH_TOKEN)); @@ -112,7 +112,7 @@ public void roundtrip() throws Exception { // token is no more valid checkUserIsNotConnected(restWc); - HtmlPage configWithoutToken = wc.goTo(u.getUrl() + "/configure"); + HtmlPage configWithoutToken = wc.goTo(u.getUrl() + "/security/"); assertEquals(200, configWithoutToken.getWebResponse().getStatusCode()); assertThat(configWithoutToken.getWebResponse().getContentAsString(), not(containsString(tokenUuid))); assertThat(configWithoutToken.getWebResponse().getContentAsString(), not(containsString(tokenName))); diff --git a/test/src/test/java/jenkins/security/seed/UserSeedPropertyTest.java b/test/src/test/java/jenkins/security/seed/UserSeedPropertyTest.java index d87761ad391c..a9a3c8830a53 100644 --- a/test/src/test/java/jenkins/security/seed/UserSeedPropertyTest.java +++ b/test/src/test/java/jenkins/security/seed/UserSeedPropertyTest.java @@ -256,7 +256,7 @@ public void userSeedSection_isCorrectlyDisplayed() throws Exception { User alice = User.getById(ALICE, false); assertNotNull(alice); - HtmlPage htmlPage = wc.goTo(alice.getUrl() + "/configure"); + HtmlPage htmlPage = wc.goTo(alice.getUrl() + "/security/"); htmlPage.getDocumentElement().getOneHtmlElementByAttribute("div", "class", "user-seed-panel"); } @@ -280,7 +280,7 @@ public void userSeedSection_isCorrectlyHidden_withSpecificSetting() throws Excep User alice = User.getById(ALICE, false); assertNotNull(alice); - HtmlPage htmlPage = wc.goTo(alice.getUrl() + "/configure"); + HtmlPage htmlPage = wc.goTo(alice.getUrl() + "/security/"); assertThrows("Seed section should not be displayed", ElementNotFoundException.class, () -> htmlPage.getDocumentElement().getOneHtmlElementByAttribute("div", "class", "user-seed-panel")); } finally { diff --git a/test/src/test/java/jenkins/security/stapler/DoActionFilterTest.java b/test/src/test/java/jenkins/security/stapler/DoActionFilterTest.java index 042462ca75d6..c6584f673e14 100644 --- a/test/src/test/java/jenkins/security/stapler/DoActionFilterTest.java +++ b/test/src/test/java/jenkins/security/stapler/DoActionFilterTest.java @@ -23,6 +23,7 @@ import org.htmlunit.util.NameValuePair; import org.junit.Test; import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.CapturedParameterNames; @@ -87,17 +88,19 @@ public String doValue() { @Test public void testProtectedMethodDispatch() throws Exception { - try { - wc.goTo("testAccessModifierUrl/public/value", null); - } catch (FailingHttpStatusCodeException e) { - throw new AssertionError("should have access to a public method", e); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + try { + wc.goTo("testAccessModifierUrl/public/value", null); + } catch (FailingHttpStatusCodeException e) { + throw new AssertionError("should have access to a public method", e); + } + FailingHttpStatusCodeException x = assertThrows("should not have allowed protected access", FailingHttpStatusCodeException.class, () -> wc.goTo("testAccessModifierUrl/protected/value", null)); + assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode()); + x = assertThrows("should not have allowed internal access", FailingHttpStatusCodeException.class, () -> wc.goTo("testAccessModifierUrl/internal/value", null)); + assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode()); + x = assertThrows("should not have allowed private access", FailingHttpStatusCodeException.class, () -> wc.goTo("testAccessModifierUrl/private/value", null)); + assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode()); } - FailingHttpStatusCodeException x = assertThrows("should not have allowed protected access", FailingHttpStatusCodeException.class, () -> wc.goTo("testAccessModifierUrl/protected/value", null)); - assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode()); - x = assertThrows("should not have allowed internal access", FailingHttpStatusCodeException.class, () -> wc.goTo("testAccessModifierUrl/internal/value", null)); - assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode()); - x = assertThrows("should not have allowed private access", FailingHttpStatusCodeException.class, () -> wc.goTo("testAccessModifierUrl/private/value", null)); - assertEquals(HttpServletResponse.SC_NOT_FOUND, x.getStatusCode()); } //================================= doXxx methods ================================= @@ -354,15 +357,19 @@ public void testAnnotatedMethodOk_annotatedJsonResponse() throws Exception { WebRequest settings = new WebRequest(new URL(j.getURL(), "testNewRulesOk/annotatedJsonResponse/")); settings.setHttpMethod(HttpMethod.POST); settings.setRequestBody(JSONObject.fromObject(Collections.emptyMap()).toString()); - Page page = wc.getPage(settings); - assertEquals(200, page.getWebResponse().getStatusCode()); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + Page page = wc.getPage(settings); + assertEquals(200, page.getWebResponse().getStatusCode()); + } } @Test public void testAnnotatedMethodOk_annotatedLimitedTo() { - FailingHttpStatusCodeException e = assertThrows(FailingHttpStatusCodeException.class, () -> wc.getPage(new URL(j.getURL(), "testNewRulesOk/annotatedLimitedTo/"))); - assertEquals(500, e.getStatusCode()); - assertTrue(e.getResponse().getContentAsString().contains("Needs to be in role")); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + FailingHttpStatusCodeException e = assertThrows(FailingHttpStatusCodeException.class, () -> wc.getPage(new URL(j.getURL(), "testNewRulesOk/annotatedLimitedTo/"))); + assertEquals(500, e.getStatusCode()); + assertTrue(e.getResponse().getContentAsString().contains("Needs to be in role")); + } } @Test diff --git a/test/src/test/java/jenkins/security/stapler/StaplerAbstractTest.java b/test/src/test/java/jenkins/security/stapler/StaplerAbstractTest.java index 4c3f5d28b6a2..b36b0de579be 100644 --- a/test/src/test/java/jenkins/security/stapler/StaplerAbstractTest.java +++ b/test/src/test/java/jenkins/security/stapler/StaplerAbstractTest.java @@ -55,8 +55,6 @@ public abstract class StaplerAbstractTest { public static JenkinsRule rule = new JenkinsRule(); protected JenkinsRule j; - protected JenkinsRule.WebClient wc; - protected WebApp webApp; protected static boolean filteredGetMethodTriggered = false; @@ -67,7 +65,6 @@ public abstract class StaplerAbstractTest { public void setUp() throws Exception { j = rule; j.jenkins.setCrumbIssuer(null); - wc = j.createWebClient(); this.webApp = (WebApp) j.jenkins.servletContext.getAttribute(WebApp.class.getName()); @@ -176,7 +173,7 @@ protected void assertFieldRequestWasNotBlocked() { } protected void assertReachable(String url, HttpMethod method) throws IOException { - try { + try (JenkinsRule.WebClient wc = j.createWebClient()) { Page page = wc.getPage(new WebRequest(new URL(j.getURL(), url), method)); assertEquals(200, page.getWebResponse().getStatusCode()); assertThat(page.getWebResponse().getContentAsString(), startsWith("ok")); @@ -194,14 +191,16 @@ protected void assertReachable(String url) throws IOException { } protected void assertReachableWithSettings(WebRequest request) throws IOException { - Page page = wc.getPage(request); - assertEquals(200, page.getWebResponse().getStatusCode()); - assertEquals("ok", page.getWebResponse().getContentAsString()); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + Page page = wc.getPage(request); + assertEquals(200, page.getWebResponse().getStatusCode()); + assertEquals("ok", page.getWebResponse().getContentAsString()); + } assertDoActionRequestWasNotBlocked(); } protected void assertReachableWithoutOk(String url) throws IOException { - try { + try (JenkinsRule.WebClient wc = j.createWebClient()) { Page page = wc.getPage(new URL(j.getURL(), url)); assertEquals(200, page.getWebResponse().getStatusCode()); } catch (FailingHttpStatusCodeException e) { @@ -210,7 +209,9 @@ protected void assertReachableWithoutOk(String url) throws IOException { } protected void assertNotReachable(String url) throws IOException { - FailingHttpStatusCodeException e = assertThrows("Url " + url + " is reachable but should not be, a not-found error is expected", FailingHttpStatusCodeException.class, () -> wc.getPage(new URL(j.getURL(), url))); - assertEquals("Url " + url + " returns an error different from 404", 404, e.getResponse().getStatusCode()); + try (JenkinsRule.WebClient wc = j.createWebClient()) { + FailingHttpStatusCodeException e = assertThrows("Url " + url + " is reachable but should not be, a not-found error is expected", FailingHttpStatusCodeException.class, () -> wc.getPage(new URL(j.getURL(), url))); + assertEquals("Url " + url + " returns an error different from 404", 404, e.getResponse().getStatusCode()); + } } } diff --git a/test/src/test/java/jenkins/security/stapler/StaplerDispatchValidatorTest.java b/test/src/test/java/jenkins/security/stapler/StaplerDispatchValidatorTest.java index c5b523073f89..0483b2825e43 100644 --- a/test/src/test/java/jenkins/security/stapler/StaplerDispatchValidatorTest.java +++ b/test/src/test/java/jenkins/security/stapler/StaplerDispatchValidatorTest.java @@ -109,15 +109,6 @@ public void canViewPagesThatRedirectToViews() throws Exception { } } - @Test - public void canViewCompressedViews() throws Exception { - String[] urls = {"groovy/compress", "jelly/compress"}; - for (String url : urls) { - HtmlPage root = j.createWebClient().goTo(url); - assertEquals("Fragment", root.getElementById("frag").asNormalizedText()); - } - } - @Test public void cannotViewFragment() throws Exception { String[] urls = {"annotated/frag", "groovy/frag", "jelly/frag", "whitelist/frag"}; diff --git a/test/src/test/java/jenkins/util/SystemPropertiesTest.java b/test/src/test/java/jenkins/util/SystemPropertiesTest.java index 4a0a2a78e981..d0b498f88e57 100644 --- a/test/src/test/java/jenkins/util/SystemPropertiesTest.java +++ b/test/src/test/java/jenkins/util/SystemPropertiesTest.java @@ -29,7 +29,7 @@ import static org.hamcrest.Matchers.nullValue; import javax.servlet.ServletContextEvent; -import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assume; @@ -103,7 +103,7 @@ public void shouldReturnWebAppPropertyIfSystemPropertyNotSetAndDefaultIsSet() th * @param value value of the property */ protected void setWebAppInitParameter(String property, String value) { - Assume.assumeThat(j.jenkins.servletContext, Matchers.instanceOf(ContextHandler.Context.class)); - ((ContextHandler.Context) j.jenkins.servletContext).getContextHandler().getInitParams().put(property, value); + Assume.assumeThat(j.jenkins.servletContext, Matchers.instanceOf(WebAppContext.Context.class)); + ((WebAppContext.Context) j.jenkins.servletContext).getContextHandler().getInitParams().put(property, value); } } diff --git a/test/src/test/java/jenkins/widgets/BuildTimeTrendTest.java b/test/src/test/java/jenkins/widgets/BuildTimeTrendTest.java index cd729ed55668..38823a27a99a 100644 --- a/test/src/test/java/jenkins/widgets/BuildTimeTrendTest.java +++ b/test/src/test/java/jenkins/widgets/BuildTimeTrendTest.java @@ -74,7 +74,7 @@ public void withAbstractJob_OnBuiltInNode() throws Exception { wc.withThrowExceptionOnFailingStatusCode(false); HtmlPage page = wc.getPage(p, "buildTimeTrend"); - HtmlTable table = page.getDocumentElement().querySelector("table[data-is-distributed-build-enabled=false]"); + HtmlTable table = page.getDocumentElement().querySelector("table[data-show-agent=false]"); assertNotNull(table); } @@ -91,7 +91,7 @@ public void withAbstractJob_OnAgentNode() throws Exception { wc.withThrowExceptionOnFailingStatusCode(false); HtmlPage page = wc.getPage(p, "buildTimeTrend"); - DomNodeList anchors = page.getDocumentElement().querySelectorAll("table[data-is-distributed-build-enabled=true] td a"); + DomNodeList anchors = page.getDocumentElement().querySelectorAll("table[data-show-agent=true] td a"); Optional anchor = anchors.stream() .filter(a -> a.getTextContent().equals(agent.getNodeName())) .findFirst(); @@ -115,7 +115,7 @@ public void withAbstractJob_OnBoth() throws Exception { wc.withThrowExceptionOnFailingStatusCode(false); HtmlPage page = wc.getPage(p, "buildTimeTrend"); - DomNodeList anchors = page.getDocumentElement().querySelectorAll("table[data-is-distributed-build-enabled=true] td a"); + DomNodeList anchors = page.getDocumentElement().querySelectorAll("table[data-show-agent=true] td a"); Optional anchor = anchors.stream() .filter(a -> a.getTextContent().equals(agent.getNodeName())) .findFirst(); @@ -123,7 +123,7 @@ public void withAbstractJob_OnBoth() throws Exception { assertTrue(anchor.isPresent()); String builtInNode = hudson.model.Messages.Hudson_Computer_DisplayName(); - DomNodeList tds = page.getDocumentElement().querySelectorAll("table[data-is-distributed-build-enabled=true] td"); + DomNodeList tds = page.getDocumentElement().querySelectorAll("table[data-show-agent=true] td"); Optional td = tds.stream() .filter(t -> t.getTextContent().equals(builtInNode)) .findFirst(); @@ -142,7 +142,7 @@ public void withNonAbstractJob_withoutAgents() throws Exception { wc.withThrowExceptionOnFailingStatusCode(false); HtmlPage page = wc.getPage(p, "buildTimeTrend"); - DomNodeList tds = page.getDocumentElement().querySelectorAll("table[data-is-distributed-build-enabled=false] td"); + DomNodeList tds = page.getDocumentElement().querySelectorAll("table[data-show-agent=false] td"); Optional td = tds.stream() .filter(t -> t.getTextContent().equals("#1")) .findFirst(); @@ -168,7 +168,7 @@ public void withNonAbstractJob_withAgents() throws Exception { wc.withThrowExceptionOnFailingStatusCode(false); HtmlPage page = wc.getPage(p, "buildTimeTrend"); - DomNodeList tds = page.getDocumentElement().querySelectorAll("table[data-is-distributed-build-enabled=true] td"); + DomNodeList tds = page.getDocumentElement().querySelectorAll("table[data-show-agent=false] td"); Optional td = tds.stream() .filter(t -> t.getTextContent().equals("#1")) .findFirst(); diff --git a/test/src/test/java/lib/form/SecretTextareaTest.java b/test/src/test/java/lib/form/SecretTextareaTest.java index a0d68fbe1132..5ecfeae1f8da 100644 --- a/test/src/test/java/lib/form/SecretTextareaTest.java +++ b/test/src/test/java/lib/form/SecretTextareaTest.java @@ -37,7 +37,7 @@ import java.io.IOException; import org.htmlunit.html.HtmlElement; import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlHiddenInput; +import org.htmlunit.html.HtmlTextArea; import org.htmlunit.html.HtmlTextInput; import org.junit.Before; import org.junit.Rule; @@ -120,8 +120,8 @@ private static void clickSecretUpdateButton(HtmlForm configForm) throws IOExcept } private static String getHiddenSecretValue(HtmlForm configForm) { - HtmlHiddenInput hiddenSecret = configForm.getInputByName("_.secret"); - return hiddenSecret == null ? null : hiddenSecret.getValue(); + HtmlTextArea hiddenSecret = configForm.getTextAreaByName("_.secret"); + return hiddenSecret == null ? null : hiddenSecret.getTextContent(); } public static class TestBuilder extends Builder { @@ -144,7 +144,7 @@ private static TestBuilder fromStringWithDescription(String secret, String descr @DataBoundConstructor public TestBuilder(Secret secret) { - this.secret = secret; + this.secret = fixEmptySecret(secret); } public Secret getSecret() { @@ -160,6 +160,13 @@ public void setDescription(String description) { this.description = description; } + private static Secret fixEmptySecret(Secret possiblyEmpty) { + if (possiblyEmpty == null || possiblyEmpty.getPlainText().isEmpty()) { + return null; + } + return possiblyEmpty; + } + @TestExtension public static class DescriptorImpl extends BuildStepDescriptor { @NonNull diff --git a/test/src/test/java/org/jenkins/ui/symbol/SymbolJenkinsTest.java b/test/src/test/java/org/jenkins/ui/symbol/SymbolJenkinsTest.java index 1609e5ff0e3c..49aaa9cd4f52 100644 --- a/test/src/test/java/org/jenkins/ui/symbol/SymbolJenkinsTest.java +++ b/test/src/test/java/org/jenkins/ui/symbol/SymbolJenkinsTest.java @@ -7,6 +7,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.DisplayName; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.RealJenkinsRule; @@ -15,6 +16,22 @@ public class SymbolJenkinsTest { public RealJenkinsRule rjr = new RealJenkinsRule() .addPlugins("plugins/design-library.jpi", "plugins/prism-api.jpi", "plugins/bootstrap5-api.jpi"); + @Test + @Issue("JENKINS-73243") + @DisplayName("When resolving a symbol where the tooltip contains '$' no error is thrown") + public void dollarInToolTipSucceeds() throws Throwable { + rjr.then(SymbolJenkinsTest::_dollarInTooltipSucceeds); + } + + private static void _dollarInTooltipSucceeds(JenkinsRule j) { + String symbol = Symbol.get(new SymbolRequest.Builder() + .withName("add") + .withTooltip("$test") + .build() + ); + assertThat(symbol, containsString("tooltip=\"$test\"")); + } + @Test @DisplayName("When resolving a symbol from a missing plugin, the placeholder is generated instead") public void missingSymbolFromPluginDefaultsToPlaceholder() throws Throwable { diff --git a/test/src/test/resources/hudson/util/FormFieldValidatorTest/ValidatingDescribable/config.jelly b/test/src/test/resources/hudson/util/FormFieldValidatorTest/ValidatingDescribable/config.jelly new file mode 100644 index 000000000000..c560072d1ed4 --- /dev/null +++ b/test/src/test/resources/hudson/util/FormFieldValidatorTest/ValidatingDescribable/config.jelly @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/compress.jelly b/test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/compress.jelly deleted file mode 100644 index 8ef1c236f51c..000000000000 --- a/test/src/test/resources/jenkins/security/stapler/StaplerDispatchValidatorTest/Jelly/compress.jelly +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - diff --git a/war/.eslintignore b/war/.eslintignore deleted file mode 100644 index e7574ace3dd4..000000000000 --- a/war/.eslintignore +++ /dev/null @@ -1,14 +0,0 @@ -work -target -/rebel.xml - -# Node -node/ -node_modules/ - -# Generated JavaScript Bundles -jsbundles - -# External scripts -src/main/webapp/scripts/yui -src/main/js/plugin-setup-wizard/bootstrap-detached.js diff --git a/war/.eslintrc.js b/war/.eslintrc.js deleted file mode 100644 index 2df19942bd80..000000000000 --- a/war/.eslintrc.js +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-env node */ -module.exports = { - env: { - browser: true, - es2022: true, - }, - // Uses eslint default ruleset - extends: ["eslint:recommended", "prettier"], - parserOptions: { - sourceType: "module", - ecmaVersion: 2022, - }, - rules: { - curly: "error", - }, - globals: { - applyTooltip: "readonly", - AutoScroller: "readonly", - Behaviour: "readonly", - breadcrumbs: "readonly", - buildFormTree: "readonly", - CodeMirror: "readonly", - ComboBox: "readonly", - COMBOBOX_VERSION: "writeable", - createSearchBox: "readonly", - crumb: "readonly", - dialog: "readonly", - ensureVisible: "readonly", - escapeHTML: "readonly", - findAncestor: "readonly", - findAncestorClass: "readonly", - findElementsBySelector: "readonly", - findFormParent: "readonly", - fireEvent: "readonly", - Form: "readonly", - getElementOverflowParams: "readonly", - hoverNotification: "readonly", - iota: "writeable", - isInsideRemovable: "readonly", - isPageVisible: "readonly", - isRunAsTest: "readonly", - layoutUpdateCallback: "readonly", - loadScript: "readonly", - makeButton: "readonly", - notificationBar: "readonly", - object: "readonly", - objectToUrlFormEncoded: "readonly", - onSetupWizardInitialized: "readonly", - refillOnChange: "readonly", - refreshPart: "readonly", - registerSortableDragDrop: "readonly", - renderOnDemand: "readonly", - rootURL: "readonly", - safeValidateButton: "readonly", - setupWizardExtensions: "readonly", - SharedArrayBuffer: "readonly", - shortenName: "readonly", - Sortable: "readonly", - toQueryString: "readonly", - ts_refresh: "readonly", - updateOptionalBlock: "readonly", - Utilities: "readonly", - UTILITIES_VERSION: "writeable", - YAHOO: "readonly", - }, -}; diff --git a/war/package.json b/war/package.json index 3abfa312674d..8e20aa87f044 100644 --- a/war/package.json +++ b/war/package.json @@ -14,38 +14,41 @@ "prod": "webpack --config webpack.config.js --mode=production", "build": "yarn prod", "start": "yarn dev --watch", - "lint:js": "eslint --config .eslintrc.js ../ --ext js && prettier --check ../", - "lint:js-ci": "eslint --config .eslintrc.js ../ --ext js -f checkstyle -o target/eslint-warnings.xml && prettier --check ../", + "lint:js": "eslint ../ && prettier --check ../", + "lint:js-ci": "eslint ../ -f checkstyle -o target/eslint-warnings.xml && prettier --check ../", "lint:css": "stylelint src/main/scss", "lint:css-ci": "stylelint src/main/scss --custom-formatter stylelint-checkstyle-reporter -o target/stylelint-warnings.xml", "lint:ci": "yarn lint:js-ci && yarn lint:css-ci", - "lint:fix": "prettier --write ../ && stylelint src/main/scss --fix && eslint --config .eslintrc.js --fix ../", + "lint:fix": "eslint --fix ../ && prettier --write ../ && stylelint src/main/scss --fix", "lint": "yarn lint:js && yarn lint:css" }, "devDependencies": { - "@babel/cli": "7.24.1", - "@babel/core": "7.24.4", - "@babel/preset-env": "7.24.4", + "@babel/cli": "7.24.8", + "@babel/core": "7.25.2", + "@babel/preset-env": "7.25.3", + "@eslint/js": "9.9.0", "babel-loader": "9.1.3", "clean-webpack-plugin": "4.0.0", - "css-loader": "7.0.0", - "css-minimizer-webpack-plugin": "6.0.0", - "eslint": "8.57.0", + "css-loader": "7.1.2", + "css-minimizer-webpack-plugin": "7.0.0", + "eslint": "9.9.0", "eslint-config-prettier": "9.1.0", + "eslint-formatter-checkstyle": "8.40.0", + "globals": "15.9.0", "handlebars-loader": "1.7.3", - "mini-css-extract-plugin": "2.8.1", - "postcss": "8.4.38", + "mini-css-extract-plugin": "2.9.1", + "postcss": "8.4.41", "postcss-loader": "8.1.1", - "postcss-preset-env": "9.5.4", + "postcss-preset-env": "10.0.2", "postcss-scss": "4.0.9", - "prettier": "3.2.5", - "sass": "1.74.1", - "sass-loader": "14.1.1", - "style-loader": "3.3.4", - "stylelint": "16.3.1", + "prettier": "3.3.3", + "sass": "1.77.8", + "sass-loader": "16.0.1", + "style-loader": "4.0.0", + "stylelint": "16.8.2", "stylelint-checkstyle-reporter": "1.0.0", - "stylelint-config-standard": "36.0.0", - "webpack": "5.91.0", + "stylelint-config-standard": "36.0.1", + "webpack": "5.93.0", "webpack-cli": "5.1.4", "webpack-remove-empty-scripts": "1.0.4" }, @@ -62,5 +65,5 @@ "defaults", "not IE 11" ], - "packageManager": "yarn@4.1.1" + "packageManager": "yarn@4.4.0" } diff --git a/war/pom.xml b/war/pom.xml index 4ecd482bebfa..d035fc0db858 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -46,13 +46,15 @@ THE SOFTWARE. localhost 8080 - 2.12.1-101.v85b_e08b_780dd - 20.12.1 + 2.13.2-125.v200281b_61d59 + + 3107.v665000b_51092 + 20.16.0 1.22.19 - 4.1.1 - f3cc0eda8e5560e529c7147565b30faa43b4e472d90e8634d7134a37c7f59781 + 4.4.0 + 5f228cb28f2edb97d8c3b667fb7b2fdcf06c46798e25ea889dad9e0b4bc2e2c1 @@ -88,6 +90,11 @@ THE SOFTWARE. websocket-jetty10 ${project.version}
+ + org.jenkins-ci.main + websocket-jetty12-ee8 + ${project.version} + + + org.apache.maven.plugins maven-enforcer-plugin @@ -145,28 +153,18 @@ THE SOFTWARE. - 1.8 + 11 - 1.8 + 11 - com.infradna.tool:bridge-method-annotation - org.antlr:antlr4 - org.jenkins-ci:annotation-indexer org.jenkins-ci:commons-jelly - org.jenkins-ci:crypto-util org.jenkins-ci.main:cli org.jenkins-ci.main:jenkins-core - org.jenkins-ci.main:remoting org.jenkins-ci.main:websocket-jetty10 + org.jenkins-ci.main:websocket-jetty12-ee8 org.jenkins-ci.main:websocket-spi - org.jenkins-ci:memory-monitor - org.jenkins-ci:symbol-annotation - org.jenkins-ci:task-reactor - org.jenkins-ci:version-number - org.jvnet.hudson:commons-jelly-tags-define org.jvnet.winp:winp - org.kohsuke:access-modifier-annotation org.kohsuke.stapler:stapler org.kohsuke.stapler:stapler-groovy org.kohsuke.stapler:stapler-jelly @@ -178,16 +176,18 @@ THE SOFTWARE. + org.apache.maven.plugins maven-compiler-plugin - 8 - 8 + 11 + 11 + org.apache.maven.plugins maven-javadoc-plugin - 8 + 11 @@ -265,7 +265,7 @@ THE SOFTWARE. org.jenkins-ci.plugins display-url-api - 2.200.vb_9327d658781 + 2.204.vf6fddd8a_8b_e9 hpi @@ -293,28 +293,28 @@ THE SOFTWARE. org.jenkins-ci.plugins matrix-project - 822.824.v14451b_c0fd42 + 832.va_66e270d2946 hpi org.jenkins-ci.plugins script-security - 1335.vf07d9ce377a_e + 1354.va_70a_fe478c7f hpi org.jenkins-ci.plugins junit - 1265.v65b_14fa_f12f0 + 1296.vb_f538b_c88630 hpi org.jenkins-ci.plugins.workflow workflow-api - 1291.v51fd2a_625da_7 + 1336.vee415d95c521 hpi @@ -343,7 +343,7 @@ THE SOFTWARE. org.jenkins-ci.plugins.workflow workflow-support - 896.v175a_a_9c5b_78f + 920.v59f71ce16f04 hpi @@ -358,15 +358,21 @@ THE SOFTWARE. io.jenkins.plugins echarts-api - 5.5.0-1 + 5.5.1-1 + hpi + + + + io.jenkins.plugins + eddsa-api + 0.3.0-4.v84c6f0f4969e hpi - io.jenkins.plugins snakeyaml-api - 2.2-111.vc6598e30cc65 + 2.2-121.v5a_68b_9300b_d4 hpi @@ -390,7 +396,7 @@ THE SOFTWARE. io.jenkins.plugins font-awesome-api - 6.5.1-3 + 6.6.0-1 hpi @@ -398,42 +404,42 @@ THE SOFTWARE. org.jenkins-ci.plugins.workflow workflow-step-api - 657.v03b_e8115821b_ + 678.v3ee58b_469476 hpi org.jenkins-ci.plugins scm-api - 689.v237b_6d3a_ef7f + 696.v778d637b_a_762 hpi org.jenkins-ci.plugins structs - 337.v1b_04ea_4df7c8 + 338.v848422169819 hpi org.jenkins-ci.plugins bouncycastle-api - 2.30.1.77-225.v26ea_c9455fd9 + 2.30.1.78.1-248.ve27176eb_46cb_ hpi org.jenkins-ci.plugins command-launcher - 107.v773860566e2e + 115.vd8b_301cc15d0 hpi org.jenkins-ci.plugins jdk-tool - 73.vddf737284550 + 80.v8a_dee33ed6f0 hpi @@ -447,28 +453,28 @@ THE SOFTWARE. org.jenkins-ci.modules sshd - 3.322.v159e91f6a_550 + 3.330.vc866a_8389b_58 hpi org.jenkins-ci.plugins trilead-api - 2.142.v748523a_76693 + 2.147.vb_73cc728a_32e hpi io.jenkins.plugins javax-activation-api - 1.2.0-6 + 1.2.0-7 hpi io.jenkins.plugins javax-mail-api - 1.6.2-9 + 1.6.2-10 hpi @@ -489,35 +495,35 @@ THE SOFTWARE. io.jenkins.plugins asm-api - 9.6-3.v2e1fa_b_338cd7 + 9.7-33.v4d23ef79fcc8 hpi io.jenkins.plugins commons-lang3-api - 3.13.0-62.v7d18e55f51e2 + 3.16.0-82.ve2b_07d659d95 hpi io.jenkins.plugins commons-text-api - 1.11.0-95.v22a_d30ee5d36 + 1.12.0-129.v99a_50df237f7 hpi io.jenkins.plugins gson-api - 2.10.1-15.v0d99f670e0a_7 + 2.11.0-41.v019fcf6125dc hpi io.jenkins.plugins ionicons-api - 70.v2959a_b_74e3cf + 74.v93d5eb_813d5f hpi @@ -630,14 +636,14 @@ THE SOFTWARE. - org.eclipse.jetty - jetty-maven-plugin - 10.0.20 + org.eclipse.jetty.ee8 + jetty-ee8-maven-plugin + 12.0.12 - manual + 0 ${host} ${port} @@ -645,7 +651,9 @@ THE SOFTWARE. default - ${basedir}/src/realm.properties + + ${basedir}/src/realm.properties + @@ -666,6 +674,12 @@ THE SOFTWARE. true true + + com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl @@ -722,30 +736,6 @@ THE SOFTWARE. - - org.apache.maven.plugins - maven-enforcer-plugin - - - - enforce-versions - - enforce - - - - - 3.1.0 - - - - [1.8.0-101,] - - - - - - org.apache.maven.plugins maven-antrun-plugin diff --git a/war/postcss.config.js b/war/postcss.config.js index 2886b7822f36..3010c66dd798 100644 --- a/war/postcss.config.js +++ b/war/postcss.config.js @@ -1,4 +1,3 @@ -/* eslint-env node */ module.exports = { parser: "postcss-scss", plugins: { diff --git a/war/src/main/java/executable/Main.java b/war/src/main/java/executable/Main.java index 9812f3d5b9e1..8a8cf67a87a5 100644 --- a/war/src/main/java/executable/Main.java +++ b/war/src/main/java/executable/Main.java @@ -40,8 +40,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.MissingResourceException; @@ -76,8 +76,7 @@ public class Main { * This list must remain synchronized with the one in {@code * JavaVersionRecommendationAdminMonitor}. */ - private static final NavigableSet SUPPORTED_JAVA_VERSIONS = - new TreeSet<>(Arrays.asList(11, 17, 21)); + private static final NavigableSet SUPPORTED_JAVA_VERSIONS = new TreeSet<>(List.of(17, 21)); /** * Sets custom session cookie name. @@ -109,16 +108,15 @@ public class Main { // Great! } else if (releaseVersion >= SUPPORTED_JAVA_VERSIONS.first()) { if (enableFutureJava) { - System.err.println( - String.format( - "Running with Java %d from %s, which is not fully supported. " - + "Continuing because %s is set. " - + "Supported Java versions are: %s. " - + "See https://jenkins.io/redirect/java-support/ for more information.", - releaseVersion, - System.getProperty("java.home"), - ENABLE_FUTURE_JAVA_CLI_SWITCH, - SUPPORTED_JAVA_VERSIONS)); + System.err.printf( + "Running with Java %d from %s, which is not fully supported. " + + "Continuing because %s is set. " + + "Supported Java versions are: %s. " + + "See https://jenkins.io/redirect/java-support/ for more information.%n", + releaseVersion, + System.getProperty("java.home"), + ENABLE_FUTURE_JAVA_CLI_SWITCH, + SUPPORTED_JAVA_VERSIONS); } else if (releaseVersion > SUPPORTED_JAVA_VERSIONS.last()) { throw new UnsupportedClassVersionError( String.format( @@ -152,25 +150,6 @@ public class Main { } } - /** - * Get the release version of the current JVM. - * - * @return The release version of the current JVM; e.g., 8, 11, or 17. - * @throws NumberFormatException If the release version could not be parsed. - */ - private static int getReleaseVersion() { - String version = System.getProperty("java.specification.version"); - version = version.trim(); - if (version.startsWith("1.")) { - String[] split = version.split("\\."); - if (split.length != 2) { - throw new NumberFormatException("Invalid Java specification version: " + version); - } - version = split[1]; - } - return Integer.parseInt(version); - } - /** * Returns true if the Java runtime version check should not be done, and any version allowed. * @@ -195,7 +174,7 @@ private static boolean hasArgument(@NonNull String argument, @NonNull String[] a justification = "User provided values for running the program") public static void main(String[] args) throws IllegalAccessException { try { - verifyJavaVersion(getReleaseVersion(), isFutureJavaEnabled(args)); + verifyJavaVersion(Runtime.version().feature(), isFutureJavaEnabled(args)); } catch (UnsupportedClassVersionError e) { System.err.println(e.getMessage()); System.err.println("See https://jenkins.io/redirect/java-support/ for more information."); @@ -206,12 +185,17 @@ public static void main(String[] args) throws IllegalAccessException { //to achieve this use --paramsFromStdIn if (hasArgument("--paramsFromStdIn", args)) { System.out.println("--paramsFromStdIn detected. Parameters are going to be read from stdin. Other parameters passed directly will be ignored."); - String argsInStdIn = readStringNonBlocking(System.in, 131072).trim(); + String argsInStdIn; + try { + argsInStdIn = new String(System.in.readNBytes(131072), StandardCharsets.UTF_8).trim(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } args = argsInStdIn.split(" +"); } // If someone just wants to know the version, print it out as soon as possible, with no extraneous file or webroot info. // This makes it easier to grab the version from a script - final List arguments = new ArrayList<>(Arrays.asList(args)); + final List arguments = new ArrayList<>(List.of(args)); if (arguments.contains("--version")) { System.out.println(getVersion("?")); return; @@ -282,7 +266,7 @@ public static void main(String[] args) throws IllegalAccessException { // locate the Winstone launcher ClassLoader cl; try { - cl = new URLClassLoader(new URL[] {tmpJar.toURI().toURL()}); + cl = new URLClassLoader("Jenkins Main ClassLoader", new URL[] {tmpJar.toURI().toURL()}, ClassLoader.getSystemClassLoader()); } catch (MalformedURLException e) { throw new UncheckedIOException(e); } @@ -365,24 +349,6 @@ public static void main(String[] args) throws IllegalAccessException { } } - /** - * Reads up to maxRead bytes from InputStream if available into a String - * - * @param in input stream to be read - * @param maxToRead maximum number of bytes to read from the in - * @return a String read from in - */ - private static String readStringNonBlocking(InputStream in, int maxToRead) { - byte[] buffer; - try { - buffer = new byte[Math.min(in.available(), maxToRead)]; - in.read(buffer); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return new String(buffer); - } - private static void trimOffOurOptions(List arguments) { arguments.removeIf(arg -> arg.startsWith("--extractedFilesFolder") || arg.startsWith("--pluginroot") || arg.startsWith(ENABLE_FUTURE_JAVA_CLI_SWITCH)); @@ -443,21 +409,13 @@ public static File whoAmI(File directory) { myself.deleteOnExit(); try (InputStream is = Main.class.getProtectionDomain().getCodeSource().getLocation().openStream(); OutputStream os = new FileOutputStream(myself)) { - copyStream(is, os); + is.transferTo(os); } catch (IOException e) { throw new UncheckedIOException(e); } return myself; } - private static void copyStream(InputStream in, OutputStream out) throws IOException { - byte[] buf = new byte[8192]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } - /** * Extract a resource from jar, mark it for deletion upon exit, and return its location. */ @@ -477,7 +435,7 @@ private static File extractFromJar(String resource, String fileName, String suff throw new UncheckedIOException("Jenkins failed to create a temporary file in " + tmpdir + ": " + e, e); } try (InputStream is = res.openStream(); OutputStream os = new FileOutputStream(tmp)) { - copyStream(is, os); + is.transferTo(os); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -524,7 +482,7 @@ private static void deleteWinstoneTempContents(File file) { /** * Determines the home directory for Jenkins. - * + *

* People makes configuration mistakes, so we are trying to be nice * with those by doing {@link String#trim()}. */ diff --git a/war/src/main/js/add-item.js b/war/src/main/js/add-item.js index c3a718d3f589..e648ecf8f3ff 100644 --- a/war/src/main/js/add-item.js +++ b/war/src/main/js/add-item.js @@ -32,7 +32,7 @@ $.when(getItems()).done(function (data) { if (desc.indexOf('<a href="') === -1) { return desc; } - // eslint-disable-next-line + // eslint-disable-next-line no-useless-escape var newDesc = desc.replace(/\</g, "<").replace(/\>/g, ">"); return newDesc; } @@ -60,6 +60,7 @@ $.when(getItems()).done(function (data) { } cleanValidationMessages(context); $(messageId).removeClass("input-message-disabled"); + enableSubmit(false); } function cleanValidationMessages(context) { @@ -69,7 +70,7 @@ $.when(getItems()).done(function (data) { } function enableSubmit(status) { - var btn = $("form .footer .btn-decorator button[type=submit]"); + var btn = $(".bottom-sticker-inner button[type=submit]"); if (status === true) { if (btn.hasClass("disabled")) { btn.removeClass("disabled"); @@ -144,8 +145,10 @@ $.when(getItems()).done(function (data) { var iconDiv = drawIcon(elem); item.appendChild(iconDiv); + var labelContainer = document.createElement("div"); + item.appendChild(labelContainer); - var label = item.appendChild(document.createElement("label")); + var label = labelContainer.appendChild(document.createElement("label")); var radio = label.appendChild(document.createElement("input")); radio.type = "radio"; @@ -157,7 +160,7 @@ $.when(getItems()).done(function (data) { displayName.appendChild(document.createTextNode(elem.displayName)); - var desc = item.appendChild(document.createElement("div")); + var desc = labelContainer.appendChild(document.createElement("div")); desc.className = "desc"; desc.innerHTML = checkForLink(elem.description); @@ -193,7 +196,10 @@ $.when(getItems()).done(function (data) { function drawIcon(elem) { var iconDiv = document.createElement("div"); - if (elem.iconClassName && elem.iconQualifiedUrl) { + if (elem.iconXml) { + iconDiv.className = "icon"; + iconDiv.innerHTML = elem.iconXml; + } else if (elem.iconClassName && elem.iconQualifiedUrl) { iconDiv.className = "icon"; var img1 = document.createElement("img"); diff --git a/war/src/main/js/app.js b/war/src/main/js/app.js index 63a35226d7d4..0ac9a97cce38 100644 --- a/war/src/main/js/app.js +++ b/war/src/main/js/app.js @@ -5,6 +5,7 @@ import Tooltips from "@/components/tooltips"; import StopButtonLink from "@/components/stop-button-link"; import ConfirmationLink from "@/components/confirmation-link"; import Dialogs from "@/components/dialogs"; +import Autocomplete from "@/components/autocomplete"; Dropdowns.init(); Notifications.init(); @@ -13,3 +14,4 @@ Tooltips.init(); StopButtonLink.init(); ConfirmationLink.init(); Dialogs.init(); +Autocomplete.init(); diff --git a/war/src/main/js/components/autocomplete/index.js b/war/src/main/js/components/autocomplete/index.js new file mode 100644 index 000000000000..9caefc6e242e --- /dev/null +++ b/war/src/main/js/components/autocomplete/index.js @@ -0,0 +1,122 @@ +import behaviorShim from "@/util/behavior-shim"; +import Utils from "@/components/dropdowns/utils"; + +function init() { + function addValue(value, item, delimiter) { + const prev = value.includes(delimiter) + ? value.substring(0, value.lastIndexOf(delimiter) + 1) + " " + : ""; + return prev + item + delimiter + " "; + } + + function validate(e) { + if (e.targetUrl) { + var method = e.getAttribute("checkMethod") || "post"; + try { + FormChecker.delayedCheck(e.targetUrl(), method, e.targetElement); + } catch (x) { + console.warn(x); + return; + } + } + } + + function convertSuggestionToItem(suggestion, e) { + const delimiter = e.getAttribute("autoCompleteDelimChar"); + const confirm = () => { + e.value = delimiter + ? addValue(e.value, suggestion.name, delimiter) + : suggestion.name; + validate(e); + e.focus(); + }; + return { + label: suggestion.name, + onClick: confirm, + onKeyPress: (evt) => { + if (evt.key == "Tab") { + confirm(); + e.dropdown.hide(); + evt.preventDefault(); + } + }, + }; + } + + function getMaxSuggestionCount(e) { + return parseInt(e.dataset["maxsuggestions"]) || 10; + } + + function createAndShowDropdown(e, suggestions) { + const items = suggestions + .splice(0, getMaxSuggestionCount(e)) + .map((s) => convertSuggestionToItem(s, e)); + if (!e.dropdown) { + Utils.generateDropdown( + e, + (instance) => { + e.dropdown = instance; + instance.popper.style.minWidth = e.offsetWidth + "px"; + }, + true, + ); + } + e.dropdown.setContent(Utils.generateDropdownItems(items, true)); + e.dropdown.show(); + } + + function updateSuggestions(e) { + const text = e.value.trim(); + const delimiter = e.getAttribute("autoCompleteDelimChar"); + const word = delimiter ? text.split(delimiter).reverse()[0].trim() : text; + if (!word) { + if (e.dropdown) { + e.dropdown.hide(); + } + return; + } + const url = + e.getAttribute("autoCompleteUrl") + "?value=" + encodeURIComponent(word); + fetch(url) + .then((rsp) => (rsp.ok ? rsp.json() : {})) + .then((response) => createAndShowDropdown(e, response.suggestions || [])); + } + + function debounce(callback) { + callback.running = false; + return () => { + if (!callback.running) { + callback.running = true; + setTimeout(() => { + callback(); + callback.running = false; + }, 300); + } + }; + } + + behaviorShim.specify( + "INPUT.auto-complete", + "input-auto-complete", + 0, + function (e) { + e.setAttribute("autocomplete", "off"); + e.dataset["hideOnClick"] = "false"; + // form field with auto-completion support + e.style.position = "relative"; + // otherwise menu won't hide on tab with nothing selected + // needs delay as without that it blocks click selection of an item + e.addEventListener("focusout", () => + setTimeout(() => e.dropdown && e.dropdown.hide(), 200), + ); + e.addEventListener( + "input", + debounce(() => { + updateSuggestions(e); + }), + ); + }, + ); +} + +export default { init }; diff --git a/war/src/main/js/components/dropdowns/hetero-list.js b/war/src/main/js/components/dropdowns/hetero-list.js index 2b216bc20168..0eab2b9ea324 100644 --- a/war/src/main/js/components/dropdowns/hetero-list.js +++ b/war/src/main/js/components/dropdowns/hetero-list.js @@ -77,11 +77,10 @@ function generateButtons() { function insert(instance, template) { let nc = document.createElement("div"); - nc.className = "repeated-chunk"; + nc.className = "repeated-chunk fade-in"; nc.setAttribute("name", template.name); nc.setAttribute("descriptorId", template.descriptorId); nc.innerHTML = template.html; - nc.style.opacity = "0"; instance.hide(); @@ -150,18 +149,9 @@ function generateButtons() { if (withDragDrop) { registerSortableDragDrop(nc); } - - new YAHOO.util.Anim( - nc, - { - opacity: { to: 1 }, - }, - 0.2, - YAHOO.util.Easing.easeIn, - ).animate(); - Behaviour.applySubtree(nc, true); ensureVisible(nc); + nc.classList.remove("fade-in"); layoutUpdateCallback.call(); }, true, diff --git a/war/src/main/js/components/dropdowns/jumplists.js b/war/src/main/js/components/dropdowns/jumplists.js index 3297da22e9ea..f831f452bafe 100644 --- a/war/src/main/js/components/dropdowns/jumplists.js +++ b/war/src/main/js/components/dropdowns/jumplists.js @@ -31,7 +31,7 @@ function generateJumplistAccessors() { */ function generateDropdowns() { behaviorShim.specify( - "li.children, #menuSelector, .jenkins-menu-dropdown-chevron", + "li.children, .jenkins-jumplist-link, #menuSelector, .jenkins-menu-dropdown-chevron", "-dropdown-", 1000, (element) => diff --git a/war/src/main/js/components/dropdowns/templates.js b/war/src/main/js/components/dropdowns/templates.js index 9b97c8cd7f66..eeef012ccf57 100644 --- a/war/src/main/js/components/dropdowns/templates.js +++ b/war/src/main/js/components/dropdowns/templates.js @@ -60,7 +60,7 @@ function menuItem(options) { ${label} ${ itemOptions.badge != null - ? `${badgeText}` + ? `${badgeText}` : `` } ${ @@ -74,7 +74,9 @@ function menuItem(options) { if (options.onClick) { item.addEventListener("click", (event) => options.onClick(event)); } - + if (options.onKeyPress) { + item.onkeypress = options.onKeyPress; + } return item; } diff --git a/war/src/main/js/components/dropdowns/utils.js b/war/src/main/js/components/dropdowns/utils.js index 8a3677c939b1..a15c4d2c2ba5 100644 --- a/war/src/main/js/components/dropdowns/utils.js +++ b/war/src/main/js/components/dropdowns/utils.js @@ -11,12 +11,13 @@ const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected"; * @param element - the element to generate the dropdown for * @param callback - called to retrieve the list of dropdown items */ -function generateDropdown(element, callback) { +function generateDropdown(element, callback, immediate) { tippy( element, Object.assign({}, Templates.dropdown(), { + hideOnClick: element.dataset["hideOnClick"] !== "false", onCreate(instance) { - instance.reference.addEventListener("mouseenter", () => { + const onload = () => { if (instance.loaded) { return; } @@ -26,7 +27,12 @@ function generateDropdown(element, callback) { }); callback(instance); - }); + }; + if (immediate) { + onload(); + } else { + instance.reference.addEventListener("mouseenter", onload); + } }, }), ); @@ -86,7 +92,10 @@ function generateDropdownItems(items, compact) { menuItems, () => menuItems.querySelectorAll(".jenkins-dropdown__item"), SELECTED_ITEM_CLASS, - (selectedItem, key) => { + (selectedItem, key, evt) => { + if (!selectedItem) { + return; + } switch (key) { case "ArrowLeft": { const root = selectedItem.closest("[data-tippy-root]"); @@ -110,6 +119,10 @@ function generateDropdownItems(items, compact) { .classList.add(SELECTED_ITEM_CLASS); break; } + default: + if (selectedItem.onkeypress) { + selectedItem.onkeypress(evt); + } } }, (container) => { diff --git a/war/src/main/js/components/tooltips/index.js b/war/src/main/js/components/tooltips/index.js index eb74340f2180..443c4179ed14 100644 --- a/war/src/main/js/components/tooltips/index.js +++ b/war/src/main/js/components/tooltips/index.js @@ -5,7 +5,6 @@ const TOOLTIP_BASE = { arrow: false, theme: "tooltip", animation: "tooltip", - appendTo: document.body, }; /** @@ -21,6 +20,10 @@ function registerTooltip(element) { const tooltip = element.getAttribute("tooltip"); const htmlTooltip = element.getAttribute("data-html-tooltip"); + let appendTo = document.body; + if (element.hasAttribute("data-tooltip-append-to-parent")) { + appendTo = "parent"; + } if ( tooltip !== null && tooltip.trim().length > 0 && @@ -40,6 +43,7 @@ function registerTooltip(element) { onHidden(instance) { instance.reference.setAttribute("title", instance.props.content); }, + appendTo: appendTo, }, TOOLTIP_BASE, ), @@ -58,6 +62,7 @@ function registerTooltip(element) { instance.reference.getAttribute("data-tooltip-interactive") === "true"; }, + appendTo: appendTo, }, TOOLTIP_BASE, ), diff --git a/war/src/main/js/filter-build-history.js b/war/src/main/js/filter-build-history.js deleted file mode 100644 index 05fa29d05fae..000000000000 --- a/war/src/main/js/filter-build-history.js +++ /dev/null @@ -1,591 +0,0 @@ -import debounce from "lodash/debounce"; - -const buildHistoryContainer = document.getElementById("buildHistory"); -const pageSearchInputContainer = buildHistoryContainer.querySelector( - ".build-search-row .jenkins-search", -); -const pageSearchInput = buildHistoryContainer.querySelector( - ".build-search-row input", -); -const buildHistoryPage = document.getElementById("buildHistoryPage"); -const properties = document.getElementById("properties"); -const ajaxUrl = buildHistoryPage.getAttribute("page-ajax"); -const nextBuild = properties.getAttribute("page-next-build"); -const noBuildsBanner = document.getElementById("no-builds"); - -const sidePanel = document.getElementById("side-panel"); -const buildHistoryPageNav = document.getElementById("buildHistoryPageNav"); - -const pageOne = buildHistoryPageNav.querySelector(".pageOne"); -const pageUp = buildHistoryPageNav.querySelector(".pageUp"); -const pageDown = buildHistoryPageNav.querySelector(".pageDown"); - -const leftRightPadding = 4; -const updateBuildsRefreshInterval = 5000; - -function updateBuilds(params) { - if (isPageVisible()) { - fetch(ajaxUrl + toQueryString(params), { - headers: { - n: buildHistoryContainer.headers[1], - }, - }).then((rsp) => { - if (rsp.ok) { - rsp.text().then((responseText) => { - var dataTable = getDataTable(buildHistoryContainer); - var rows = dataTable.rows; - - // Check there are no existing rows (except the search bar) before showing the no builds banner - if ( - rows.length <= 1 && - responseText === '

' - ) { - noBuildsBanner.style.display = "block"; - if ( - typeof params === "object" && - "search" in params && - params.search !== "" - ) { - pageSearchInputContainer.classList.remove("jenkins-hidden"); - } else { - pageSearchInputContainer.classList.add("jenkins-hidden"); - } - } else { - noBuildsBanner.style.display = "none"; - pageSearchInputContainer.classList.remove("jenkins-hidden"); - } - - //delete rows with transitive data - var firstBuildRow = 0; - if (rows[firstBuildRow].classList.contains("build-search-row")) { - firstBuildRow++; - } - while ( - rows.length > 1 && - rows[firstBuildRow].classList.contains("transitive") - ) { - rows[firstBuildRow].remove(); - } - - // insert new rows - var div = document.createElement("div"); - div.innerHTML = responseText; - Behaviour.applySubtree(div); - - var pivot = rows[firstBuildRow]; - var newDataTable = getDataTable(div); - var newRows = newDataTable.rows; - while (newRows.length > 0) { - if (pivot !== undefined) { - // The data table has rows. Insert before a "pivot" row (first row). - pivot.parentNode.insertBefore(newRows[0], pivot); - } else { - // The data table has no rows. In this case, we just add all new rows directly to the - // table, one after the other i.e. we don't insert before a "pivot" row (first row). - dataTable - .getElementsByTagName("tbody")[0] - .appendChild(newRows[0]); - } - } - - if (newDataTable.classList.contains("hasPageData")) { - buildHistoryPage.setAttribute( - "page-entry-newest", - newDataTable.getAttribute("page-entry-newest"), - ); - } - - // next update - buildHistoryContainer.headers = ["n", rsp.headers.get("n")]; - checkAllRowCellOverflows(); - createRefreshTimeout(params); - }); - } - }); - } else { - createRefreshTimeout(params); - } -} - -var buildRefreshTimeout; -function createRefreshTimeout(params) { - cancelRefreshTimeout(); - buildRefreshTimeout = window.setTimeout( - () => updateBuilds(params), - updateBuildsRefreshInterval, - ); -} - -function cancelRefreshTimeout() { - if (buildRefreshTimeout) { - window.clearTimeout(buildRefreshTimeout); - buildRefreshTimeout = undefined; - } -} - -function hasPageUp() { - return buildHistoryPage.getAttribute("page-has-up") === "true"; -} -function hasPageDown() { - return buildHistoryPage.getAttribute("page-has-down") === "true"; -} -function getNewestEntryId() { - return buildHistoryPage.getAttribute("page-entry-newest"); -} -function getOldestEntryId() { - return buildHistoryPage.getAttribute("page-entry-oldest"); -} - -function getDataTable(buildHistoryDiv) { - return buildHistoryDiv.querySelector("table.pane"); -} - -function updatePageParams(dataTable) { - buildHistoryPage.setAttribute( - "page-has-up", - dataTable.getAttribute("page-has-up"), - ); - buildHistoryPage.setAttribute( - "page-has-down", - dataTable.getAttribute("page-has-down"), - ); - buildHistoryPage.setAttribute( - "page-entry-newest", - dataTable.getAttribute("page-entry-newest"), - ); - buildHistoryPage.setAttribute( - "page-entry-oldest", - dataTable.getAttribute("page-entry-oldest"), - ); -} -function togglePageUpDown() { - buildHistoryPageNav.classList.remove("hasUpPage"); - buildHistoryPageNav.classList.remove("hasDownPage"); - if (hasPageUp()) { - buildHistoryPageNav.classList.add("hasUpPage"); - } - if (hasPageDown()) { - buildHistoryPageNav.classList.add("hasDownPage"); - } -} - -function checkRowCellOverflows(row) { - if (!row) { - return; - } - - if (row.classList.contains("overflow-checked")) { - // already done. - return; - } - - function markSingleline() { - row.classList.add("single-line"); - row.classList.remove("multi-line"); - } - function markMultiline() { - row.classList.remove("single-line"); - row.classList.add("multi-line"); - } - function indentMultiline(element) { - element.classList.add("indent-multiline"); - } - - function blockWrap(el1, el2) { - var div = document.createElement("div"); - - div.classList.add("block"); - div.classList.add("wrap"); - el1.classList.add("wrapped"); - el2.classList.add("wrapped"); - - el1.parentNode.insertBefore(div, el1); - el1.parentNode.removeChild(el1); - el2.parentNode.removeChild(el2); - div.appendChild(el1); - div.appendChild(el2); - - return div; - } - function blockUnwrap(element) { - element.querySelectorAll(".wrapped").forEach(function (wrappedEl) { - wrappedEl.parentNode.removeChild(wrappedEl); - element.parentNode.insertBefore(wrappedEl, element); - wrappedEl.classList.remove("wrapped"); - }); - element.parentNode.removeChild(element); - } - - var buildName = row.querySelector(".build-name"); - var buildDetails = row.querySelector(".build-details"); - - if (!buildName || !buildDetails) { - return; - } - - var buildControls = row.querySelector(".build-controls"); - var desc = row.querySelector(".desc"); - - function resetCellOverflows() { - markSingleline(); - - // undo block wraps - row.querySelectorAll(".block.wrap").forEach(function (blockWrap) { - blockUnwrap(blockWrap); - }); - - buildName.classList.remove("block"); - buildName.removeAttribute("style"); - buildDetails.classList.remove("block"); - buildDetails.removeAttribute("style"); - if (buildControls) { - buildControls.classList.remove("block"); - buildDetails.removeAttribute("style"); - } - } - - // Undo everything from the previous poll. - resetCellOverflows(); - - // Mark the text as multiline, if it has more than one line - if (desc) { - markMultiline(); - } - - var rowWidth = buildHistoryContainer.clientWidth; - var usableRowWidth = rowWidth - leftRightPadding * 2; - var nameOverflowParams = getElementOverflowParams(buildName); - var detailsOverflowParams = getElementOverflowParams(buildDetails); - - var controlsOverflowParams; - if (buildControls) { - controlsOverflowParams = getElementOverflowParams(buildControls); - } - - function fitToControlsHeight(element) { - if (buildControls) { - if (element.clientHeight < buildControls.clientHeight) { - element.style.height = buildControls.clientHeight.toString() + "px"; - } - } - } - - function setBuildControlWidths() { - if (buildControls) { - var buildBadge = buildControls.querySelector(".build-badge"); - - if (buildBadge) { - var buildControlsWidth = buildControls.clientWidth; - var buildBadgeWidth; - - var buildStop = buildControls.querySelector(".build-stop"); - if (buildStop) { - buildStop.style.width = "24px"; - // Minus 24 for the buildStop width, - // minus 4 for left+right padding in the controls container - buildBadgeWidth = buildControlsWidth - 24 - leftRightPadding; - if (buildControls.classList.contains("indent-multiline")) { - buildBadgeWidth = buildBadgeWidth - 20; - } - buildBadge.style.width = buildBadgeWidth + "px"; - } else { - buildBadge.style.width = "100%"; - } - } - controlsOverflowParams = getElementOverflowParams(buildControls); - } - } - setBuildControlWidths(); - - var controlsRepositioned = false; - - if (nameOverflowParams.isOverflowed || detailsOverflowParams.isOverflowed) { - // At least one of the cells (name or details) needs to move to a row of its own. - - markMultiline(); - - if (buildControls) { - // We have build controls. Lets see can we find a combination that allows the build controls - // to sit beside either the build name or the build details. - - var badgesOverflowing = false; - var nameLessThanHalf = true; - var detailsLessThanHalf = true; - var buildBadge = buildControls.querySelector(".build-badge"); - if (buildBadge) { - var badgeOverflowParams = getElementOverflowParams(buildBadge); - - if (badgeOverflowParams.isOverflowed) { - // The badges are also overflowing. In this case, we will only attempt to - // put the controls on the same line as the name or details (see below) - // if the name or details is using less than half the width of the build history - // widget. - badgesOverflowing = true; - nameLessThanHalf = - nameOverflowParams.scrollWidth < usableRowWidth / 2; - detailsLessThanHalf = - detailsOverflowParams.scrollWidth < usableRowWidth / 2; - } - } - // eslint-disable-next-line no-inner-declarations - function expandLeftWithRight( - leftCellOverFlowParams, - rightCellOverflowParams, - ) { - // Float them left and right... - leftCellOverFlowParams.element.style.float = "left"; - rightCellOverflowParams.element.style.float = "right"; - - if ( - !leftCellOverFlowParams.isOverflowed && - !rightCellOverflowParams.isOverflowed - ) { - // If neither left nor right are overflowed, just leave as is and let them float left and right. - return; - } - if ( - leftCellOverFlowParams.isOverflowed && - !rightCellOverflowParams.isOverflowed - ) { - leftCellOverFlowParams.element.style.width = - leftCellOverFlowParams.scrollWidth + "px"; - return; - } - if ( - !leftCellOverFlowParams.isOverflowed && - rightCellOverflowParams.isOverflowed - ) { - rightCellOverflowParams.element.style.width = - rightCellOverflowParams.scrollWidth + "px"; - return; - } - } - - if ( - (!badgesOverflowing || nameLessThanHalf) && - nameOverflowParams.scrollWidth + controlsOverflowParams.scrollWidth <= - usableRowWidth - ) { - // Build name and controls can go on one row (first row). Need to move build details down - // to a row of its own (second row) by making it a block element, forcing it to wrap. If there - // are controls, we move them up to position them after the build name by inserting before the - // build details. - buildDetails.classList.add("block"); - buildControls.parentNode.removeChild(buildControls); - buildDetails.parentNode.insertBefore(buildControls, buildDetails); - var wrap = blockWrap(buildName, buildControls); - wrap.classList.add("build-name-controls"); - indentMultiline(buildDetails); - nameOverflowParams = getElementOverflowParams(buildName); // recalculate - expandLeftWithRight(nameOverflowParams, controlsOverflowParams); - setBuildControlWidths(); - fitToControlsHeight(buildName); - } else if ( - (!badgesOverflowing || detailsLessThanHalf) && - detailsOverflowParams.scrollWidth + - controlsOverflowParams.scrollWidth <= - usableRowWidth - ) { - // Build details and controls can go on one row. Need to make the - // build name (first field) a block element, forcing the details and controls to wrap - // onto the next row (creating a second row). - buildName.classList.add("block"); - wrap = blockWrap(buildDetails, buildControls); - indentMultiline(wrap); - wrap.classList.add("build-details-controls"); - detailsOverflowParams = getElementOverflowParams(buildDetails); // recalculate - expandLeftWithRight(detailsOverflowParams, controlsOverflowParams); - setBuildControlWidths(); - fitToControlsHeight(buildDetails); - } else { - // No suitable combo fits on a row. All need to go on rows of their own. - buildName.classList.add("block"); - buildDetails.classList.add("block"); - buildControls.classList.add("block"); - indentMultiline(buildDetails); - indentMultiline(buildControls); - nameOverflowParams = getElementOverflowParams(buildName); // recalculate - detailsOverflowParams = getElementOverflowParams(buildDetails); // recalculate - setBuildControlWidths(); - } - controlsRepositioned = true; - } else { - buildName.classList.add("block"); - buildDetails.classList.add("block"); - indentMultiline(buildDetails); - } - } - - if (buildControls && !controlsRepositioned) { - buildBadge = buildControls.querySelector(".build-badge"); - if (buildBadge) { - badgeOverflowParams = getElementOverflowParams(buildBadge); - - if (badgeOverflowParams.isOverflowed) { - markMultiline(); - indentMultiline(buildControls); - buildControls.classList.add("block"); - controlsRepositioned = true; - setBuildControlWidths(); - } - } - } - - if ( - !nameOverflowParams.isOverflowed && - !detailsOverflowParams.isOverflowed && - !controlsRepositioned - ) { - fitToControlsHeight(buildName); - fitToControlsHeight(buildDetails); - } - - row.classList.add("overflow-checked"); -} - -function checkAllRowCellOverflows() { - if (isRunAsTest) { - return; - } - - var dataTable = getDataTable(buildHistoryContainer); - var rows = dataTable.rows; - - for (var i = 0; i < rows.length; i++) { - var row = rows[i]; - checkRowCellOverflows(row); - } -} - -function loadPage(params, focusOnSearch) { - var searchString = pageSearchInput.value; - - if (searchString !== "") { - if (params === undefined) { - params = {}; - } - params.search = searchString; - } - - fetch(ajaxUrl + toQueryString(params)).then((rsp) => { - if (rsp.ok) { - rsp.text().then((responseText) => { - pageSearchInputContainer.classList.remove("jenkins-search--loading"); - buildHistoryContainer.classList.remove("jenkins-pane--loading"); - - if (responseText === '
') { - noBuildsBanner.style.display = "block"; - if ( - typeof params === "object" && - "search" in params && - params.search !== "" - ) { - pageSearchInputContainer.classList.remove("jenkins-hidden"); - } else { - pageSearchInputContainer.classList.add("jenkins-hidden"); - } - } else { - noBuildsBanner.style.display = "none"; - pageSearchInputContainer.classList.remove("jenkins-hidden"); - } - - var dataTable = getDataTable(buildHistoryContainer); - var tbody = dataTable.getElementsByTagName("tbody")[0]; - var rows = tbody.getElementsByClassName("build-row"); - - // Delete all build rows - while (rows.length > 0) { - rows[0].remove(); - } - - // insert new rows - var div = document.createElement("div"); - div.innerHTML = responseText; - Behaviour.applySubtree(div); - - var newDataTable = getDataTable(div); - var newRows = newDataTable.rows; - while (newRows.length > 0) { - tbody.appendChild(newRows[0]); - } - - checkAllRowCellOverflows(); - updatePageParams(newDataTable); - togglePageUpDown(); - if (!hasPageUp()) { - createRefreshTimeout(params); - } - - if (focusOnSearch) { - pageSearchInput.focus(); - } - }); - } - }); -} - -const handleFilter = function () { - loadPage({}, true); -}; - -const debouncedFilter = debounce(handleFilter, 300); - -document.addEventListener("DOMContentLoaded", function () { - // Apply correct styling upon filter bar text change, call API after wait - if (pageSearchInput !== null) { - pageSearchInput.addEventListener("input", function () { - pageSearchInputContainer.classList.add("jenkins-search--loading"); - buildHistoryContainer.classList.add("jenkins-pane--loading"); - noBuildsBanner.style.display = "none"; - - debouncedFilter(); - }); - } - - if (isRunAsTest) { - return; - } - - // If the build history pane is collapsed, just return immediately and don't set up - // the build history refresh. - if (buildHistoryContainer.classList.contains("collapsed")) { - return; - } - - buildHistoryContainer.headers = ["n", nextBuild]; - - createRefreshTimeout(); - checkAllRowCellOverflows(); - - // Show/hide the nav as the mouse moves into the sidepanel and build history. - sidePanel.addEventListener("mouseover", function () { - buildHistoryPageNav.classList.add("mouseOverSidePanel"); - }); - sidePanel.addEventListener("mouseout", function () { - buildHistoryPageNav.classList.remove("mouseOverSidePanel"); - }); - buildHistoryContainer.addEventListener("mouseover", function () { - buildHistoryPageNav.classList.add("mouseOverSidePanelBuildHistory"); - }); - buildHistoryContainer.addEventListener("mouseout", function () { - buildHistoryPageNav.classList.remove("mouseOverSidePanelBuildHistory"); - }); - - pageOne.addEventListener("click", function () { - loadPage(); - }); - pageUp.addEventListener("click", function () { - loadPage({ "newer-than": getNewestEntryId() }); - }); - pageDown.addEventListener("click", function () { - if (hasPageDown()) { - cancelRefreshTimeout(); - loadPage({ "older-than": getOldestEntryId() }); - } else { - // wrap back around to the top - loadPage(); - } - }); - - togglePageUpDown(); -}); diff --git a/war/src/main/js/pages/project/builds-card.js b/war/src/main/js/pages/project/builds-card.js new file mode 100644 index 000000000000..bea375d337d8 --- /dev/null +++ b/war/src/main/js/pages/project/builds-card.js @@ -0,0 +1,142 @@ +import debounce from "lodash/debounce"; +import behaviorShim from "@/util/behavior-shim"; + +// Card/item controls +const buildHistoryPage = document.getElementById("buildHistoryPage"); +const pageSearch = buildHistoryPage.querySelector(".jenkins-search"); +const pageSearchInput = buildHistoryPage.querySelector("input"); +const ajaxUrl = buildHistoryPage.getAttribute("page-ajax"); +const card = document.querySelector("#jenkins-builds"); +const contents = card.querySelector("#jenkins-build-history"); +const container = card.querySelector(".app-builds-container"); +const noBuilds = card.querySelector("#no-builds"); + +// Pagination controls +const paginationControls = document.querySelector("#controls"); +const paginationPrevious = document.querySelector("#up"); +const paginationNext = document.querySelector("#down"); + +// Refresh variables +let buildRefreshTimeout; +const updateBuildsRefreshInterval = 5000; + +/** + * Refresh the 'Builds' card + * @param {QueryParameters} options + */ +function load(options = {}) { + /** @type {QueryParameters} */ + const params = Object.assign({}, options, { search: pageSearchInput.value }); + + // Avoid fetching if the page isn't active + if (document.hidden) { + return; + } + + fetch(ajaxUrl + toQueryString(params)).then((rsp) => { + if (rsp.ok) { + rsp.text().then((responseText) => { + container.classList.remove("app-builds-container--loading"); + pageSearch.classList.remove("jenkins-search--loading"); + + // Show the 'No builds' text if there are no builds + if (responseText.trim() === "") { + contents.innerHTML = ""; + noBuilds.style.display = "block"; + updateCardControls({ + pageHasUp: false, + pageHasDown: false, + pageEntryNewest: false, + pageEntryOldest: false, + }); + return; + } + + // Show the refreshed builds list + contents.innerHTML = responseText; + noBuilds.style.display = "none"; + behaviorShim.applySubtree(contents); + + // Show the card controls + const div = document.createElement("div"); + div.innerHTML = responseText; + const innerChild = div.children[0]; + updateCardControls({ + pageHasUp: innerChild.dataset.pageHasUp === "true", + pageHasDown: innerChild.dataset.pageHasDown === "true", + pageEntryNewest: innerChild.dataset.pageEntryNewest, + pageEntryOldest: innerChild.dataset.pageEntryOldest, + }); + }); + } else { + console.error("Failed to load 'Builds' card, response from API is:", rsp); + } + }); +} + +/** + * Shows/hides the card's pagination controls depending on the passed parameter + * @param {CardControlsOptions} parameters + */ +function updateCardControls(parameters) { + paginationControls.classList.toggle( + "jenkins-hidden", + !parameters.pageHasUp && !parameters.pageHasDown, + ); + paginationPrevious.classList.toggle( + "app-builds-container__button--disabled", + !parameters.pageHasUp, + ); + paginationNext.classList.toggle( + "app-builds-container__button--disabled", + !parameters.pageHasDown, + ); + + // We only want the list to refresh if the user is on the first page of results + if (!parameters.pageHasUp) { + createRefreshTimeout(); + } else { + cancelRefreshTimeout(); + } + + buildHistoryPage.dataset.pageEntryNewest = parameters.pageEntryNewest; + buildHistoryPage.dataset.pageEntryOldest = parameters.pageEntryOldest; +} + +paginationPrevious.addEventListener("click", () => { + load({ "newer-than": buildHistoryPage.dataset.pageEntryNewest }); +}); + +paginationNext.addEventListener("click", () => { + cancelRefreshTimeout(); + load({ "older-than": buildHistoryPage.dataset.pageEntryOldest }); +}); + +function createRefreshTimeout() { + cancelRefreshTimeout(); + buildRefreshTimeout = window.setTimeout( + () => load(), + updateBuildsRefreshInterval, + ); +} + +function cancelRefreshTimeout() { + if (buildRefreshTimeout) { + window.clearTimeout(buildRefreshTimeout); + buildRefreshTimeout = undefined; + } +} + +const debouncedLoad = debounce(() => { + load(); +}, 150); + +document.addEventListener("DOMContentLoaded", function () { + pageSearchInput.addEventListener("input", function () { + container.classList.add("app-builds-container--loading"); + pageSearch.classList.add("jenkins-search--loading"); + debouncedLoad(); + }); + + load(); +}); diff --git a/war/src/main/js/pages/project/builds-card.types.js b/war/src/main/js/pages/project/builds-card.types.js new file mode 100644 index 000000000000..4745f088bd43 --- /dev/null +++ b/war/src/main/js/pages/project/builds-card.types.js @@ -0,0 +1,16 @@ +/** + * @typedef QueryParameters + * @type {object} + * @property {string | undefined} search + * @property {string | undefined} older-than + * @property {string | undefined} newer-than + */ + +/** + * @typedef CardControlsOptions + * @type {object} + * @property {boolean} pageHasUp + * @property {boolean} pageHasDown + * @property {string | undefined} pageEntryNewest + * @property {string | undefined} pageEntryOldest + */ diff --git a/war/src/main/js/pluginSetupWizardGui.js b/war/src/main/js/pluginSetupWizardGui.js index 7d123e39876e..f2ae7fa8c6fc 100644 --- a/war/src/main/js/pluginSetupWizardGui.js +++ b/war/src/main/js/pluginSetupWizardGui.js @@ -288,6 +288,7 @@ var createPluginSetupWizard = function (appendTarget) { if (document.activeElement !== e) { e.focus(); } + // eslint-disable-next-line no-unused-vars } catch (ex) { // ignored, unable to restore focus } diff --git a/war/src/main/js/templates/plugin-manager/available.hbs b/war/src/main/js/templates/plugin-manager/available.hbs index 2810b7a13d9b..115a7b4d8e21 100644 --- a/war/src/main/js/templates/plugin-manager/available.hbs +++ b/war/src/main/js/templates/plugin-manager/available.hbs @@ -31,12 +31,12 @@
{{/if}} {{#if this.newerCoreRequired }} -
+
{{{ this.newerCoreRequired }}}
{{/if}} {{#if this.unresolvedSecurityWarnings }} -
+
{{ this.unresolvedSecurityWarnings.text }}
    {{#each this.unresolvedSecurityWarnings.warnings}} @@ -50,12 +50,12 @@
{{/if}} {{#if this.deprecated }} -
+
{{{ this.deprecated }}}
{{/if}} {{#if this.adoptMe }} -
+
{{{ this.adoptMe }}}
{{/if}} diff --git a/war/src/main/js/util/behavior-shim.js b/war/src/main/js/util/behavior-shim.js index 273efd6670d1..d605ecae33f9 100644 --- a/war/src/main/js/util/behavior-shim.js +++ b/war/src/main/js/util/behavior-shim.js @@ -1,10 +1,8 @@ function specify(selector, id, priority, behavior) { - // eslint-ignore-next-line Behaviour.specify(selector, id, priority, behavior); } function applySubtree(startNode, includeSelf) { - // eslint-ignore-next-line Behaviour.applySubtree(startNode, includeSelf); } diff --git a/war/src/main/js/util/keyboard.js b/war/src/main/js/util/keyboard.js index 0196d8992f83..88d677530416 100644 --- a/war/src/main/js/util/keyboard.js +++ b/war/src/main/js/util/keyboard.js @@ -74,7 +74,7 @@ export default function makeKeyboardNavigable( selectedItem.click(); } } else { - additionalBehaviours(selectedItem, e.key); + additionalBehaviours(selectedItem, e.key, e); } } }); diff --git a/war/src/main/js/widgets/add/addform.scss b/war/src/main/js/widgets/add/addform.scss index ecd284250b1a..2610a3cec02f 100644 --- a/war/src/main/js/widgets/add/addform.scss +++ b/war/src/main/js/widgets/add/addform.scss @@ -59,16 +59,13 @@ } .default-icon { - position: absolute; - left: 10px; - top: 10px; - height: 48px; - width: 48px; + height: 40px; + width: 40px; border-radius: 50%; text-align: center; - line-height: 48px; + line-height: 36px; font-weight: bold; - font-size: 175%; + font-size: 125%; color: #ffffff; text-shadow: rgba(0, 0, 0, 0.25) 0 -1px 1px; opacity: 0.75; diff --git a/war/src/main/resources/images/symbols/error.svg b/war/src/main/resources/images/symbols/error.svg new file mode 100644 index 000000000000..1c236a7a2cf1 --- /dev/null +++ b/war/src/main/resources/images/symbols/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/expand.svg b/war/src/main/resources/images/symbols/expand.svg new file mode 100644 index 000000000000..0ad346df7f67 --- /dev/null +++ b/war/src/main/resources/images/symbols/expand.svg @@ -0,0 +1 @@ + diff --git a/war/src/main/resources/images/symbols/flask.svg b/war/src/main/resources/images/symbols/flask.svg new file mode 100644 index 000000000000..9c4c478d820d --- /dev/null +++ b/war/src/main/resources/images/symbols/flask.svg @@ -0,0 +1 @@ +Flask \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/freestyle-project.svg b/war/src/main/resources/images/symbols/freestyle-project.svg new file mode 100644 index 000000000000..47be8c85a871 --- /dev/null +++ b/war/src/main/resources/images/symbols/freestyle-project.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/journal.svg b/war/src/main/resources/images/symbols/journal.svg index 7bd396468079..26170aed452d 100644 --- a/war/src/main/resources/images/symbols/journal.svg +++ b/war/src/main/resources/images/symbols/journal.svg @@ -1 +1 @@ -ionicons-v5-o \ No newline at end of file +ionicons-v5-o \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/lock-closed.svg b/war/src/main/resources/images/symbols/lock-closed.svg index 96a5e417f4ae..2e0cdde7c7c0 100644 --- a/war/src/main/resources/images/symbols/lock-closed.svg +++ b/war/src/main/resources/images/symbols/lock-closed.svg @@ -1 +1 @@ -ionicons-v5-g \ No newline at end of file +ionicons-v5-g \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/menu.svg b/war/src/main/resources/images/symbols/menu.svg new file mode 100644 index 000000000000..fadc2bab3081 --- /dev/null +++ b/war/src/main/resources/images/symbols/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/war/src/main/resources/images/symbols/paper-plane-outline.svg b/war/src/main/resources/images/symbols/paper-plane-outline.svg new file mode 100644 index 000000000000..5ff87689259a --- /dev/null +++ b/war/src/main/resources/images/symbols/paper-plane-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/play.svg b/war/src/main/resources/images/symbols/play.svg index a9bb95b897a9..ffc227bf89e2 100644 --- a/war/src/main/resources/images/symbols/play.svg +++ b/war/src/main/resources/images/symbols/play.svg @@ -1 +1 @@ -ionicons-v5-c \ No newline at end of file +ionicons-v5-c \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/power.svg b/war/src/main/resources/images/symbols/power.svg index cc2d4d57b63c..92886b46a09a 100644 --- a/war/src/main/resources/images/symbols/power.svg +++ b/war/src/main/resources/images/symbols/power.svg @@ -1 +1 @@ -ionicons-v5-p \ No newline at end of file +ionicons-v5-p \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/status-aborted-anime.svg b/war/src/main/resources/images/symbols/status-aborted-anime.svg index 308e6d7578c7..2dc6b8dbbf82 100644 --- a/war/src/main/resources/images/symbols/status-aborted-anime.svg +++ b/war/src/main/resources/images/symbols/status-aborted-anime.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/war/src/main/resources/images/symbols/status-blue-anime.svg b/war/src/main/resources/images/symbols/status-blue-anime.svg index b11da163e9f7..3db79b0c59f4 100644 --- a/war/src/main/resources/images/symbols/status-blue-anime.svg +++ b/war/src/main/resources/images/symbols/status-blue-anime.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/war/src/main/resources/images/symbols/status-disabled-anime.svg b/war/src/main/resources/images/symbols/status-disabled-anime.svg index 138edc7a9ec3..bcbcbc03f3ac 100644 --- a/war/src/main/resources/images/symbols/status-disabled-anime.svg +++ b/war/src/main/resources/images/symbols/status-disabled-anime.svg @@ -1,6 +1,6 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/status-nobuilt-anime.svg b/war/src/main/resources/images/symbols/status-nobuilt-anime.svg index 9a534d34d237..c066275426ab 100644 --- a/war/src/main/resources/images/symbols/status-nobuilt-anime.svg +++ b/war/src/main/resources/images/symbols/status-nobuilt-anime.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/war/src/main/resources/images/symbols/status-red-anime.svg b/war/src/main/resources/images/symbols/status-red-anime.svg index 76b3bca95af1..cdedc67bc4aa 100644 --- a/war/src/main/resources/images/symbols/status-red-anime.svg +++ b/war/src/main/resources/images/symbols/status-red-anime.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/war/src/main/resources/images/symbols/status-yellow-anime.svg b/war/src/main/resources/images/symbols/status-yellow-anime.svg index c241c3c88024..2f8051e990a8 100644 --- a/war/src/main/resources/images/symbols/status-yellow-anime.svg +++ b/war/src/main/resources/images/symbols/status-yellow-anime.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/war/src/main/resources/images/symbols/trash-bin.svg b/war/src/main/resources/images/symbols/trash-bin.svg index d8449544a7a8..572cb143609f 100644 --- a/war/src/main/resources/images/symbols/trash-bin.svg +++ b/war/src/main/resources/images/symbols/trash-bin.svg @@ -1 +1 @@ -ionicons-v5-k \ No newline at end of file +ionicons-v5-k \ No newline at end of file diff --git a/war/src/main/resources/images/symbols/weather-icon-health-00to19.svg b/war/src/main/resources/images/symbols/weather-icon-health-00to19.svg index 73e2f674a987..9e11cbcbb9eb 100644 --- a/war/src/main/resources/images/symbols/weather-icon-health-00to19.svg +++ b/war/src/main/resources/images/symbols/weather-icon-health-00to19.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/war/src/main/resources/images/symbols/weather-icon-health-20to39.svg b/war/src/main/resources/images/symbols/weather-icon-health-20to39.svg index f11877eb60db..1766284038bb 100644 --- a/war/src/main/resources/images/symbols/weather-icon-health-20to39.svg +++ b/war/src/main/resources/images/symbols/weather-icon-health-20to39.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/war/src/main/resources/images/symbols/weather-icon-health-60to79.svg b/war/src/main/resources/images/symbols/weather-icon-health-60to79.svg index bd4ed3d4c452..c6caf30230ce 100644 --- a/war/src/main/resources/images/symbols/weather-icon-health-60to79.svg +++ b/war/src/main/resources/images/symbols/weather-icon-health-60to79.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/war/src/main/scss/abstracts/_theme.scss b/war/src/main/scss/abstracts/_theme.scss index e8fab3b59b7d..cc21f1e8ab90 100644 --- a/war/src/main/scss/abstracts/_theme.scss +++ b/war/src/main/scss/abstracts/_theme.scss @@ -24,6 +24,8 @@ $semantics: ( "success": var(--green), "destructive": var(--red), "build": var(--green), + "danger": var(--red), + "info": var(--blue), ); :root, @@ -37,7 +39,7 @@ $semantics: ( --font-size-base: 1rem; // 16px --font-size-sm: 0.875rem; // 14px --font-size-xs: 0.75rem; // 12px - --font-size-monospace: 0.95em; + --font-size-monospace: 1em; // Line height --line-height-base: 1.5; @@ -50,9 +52,9 @@ $semantics: ( --dark-grey: #4d545d; // branding - --secondary: hsl(240, 10%, 50%); - --focus-input-border: #063f61; - --focus-input-glow: #{color.change(#0b6aa2, $alpha: 0.25)}; + --secondary: rgb(96, 125, 159); + --focus-input-border: var(--accent-color); + --focus-input-glow: color-mix(in sRGB, var(--accent-color) 15%, transparent); // Deprecated --focus-btn-primary: #{color.change(#0b6aa2, $alpha: 0.5)}; @@ -172,8 +174,8 @@ $semantics: ( --table-header-foreground: var(--text-color); --table-body-background: var(--background); --table-body-foreground: var(--text-color); - --table-border-radius: 10px; - --table-row-border-radius: 4px; + --table-border-radius: 0.75rem; + --table-row-border-radius: 0.3125rem; // Deprecated --even-row-color: var(--very-light-grey); @@ -195,7 +197,7 @@ $semantics: ( --link-text-decoration: none; --link-text-decoration--hover: underline; --link-text-decoration--active: underline; - --link-font-weight: 500; + --link-font-weight: 450; // Tooltips --tooltip-backdrop-filter: contrast(0.6) brightness(2.4) saturate(2) @@ -242,7 +244,7 @@ $semantics: ( --pane-link-color--visited: black; // Cards - --card-background: transparent; + --card-background: var(--background); --card-background--hover: transparent; --card-background--active: transparent; --card-border-color: hsla(240, 25%, 75%, 0.25); @@ -275,10 +277,22 @@ $semantics: ( --task-link-bg-color--hover: var(--very-light-grey); // Form - --section-padding: 1.75rem; - --input-color: var(--white); - --input-border: #c3ccd1; - --input-border-hover: #5c7889; + --section-padding: 1.625rem; + --input-color: color-mix( + in sRGB, + var(--text-color-secondary) 1.5%, + var(--background) + ); + --input-border: color-mix( + in sRGB, + var(--text-color-secondary) 25%, + transparent + ); + --input-border-hover: color-mix( + in sRGB, + var(--text-color-secondary) 50%, + transparent + ); --input-hidden-password-bg-color: #f9f9f9; --form-item-max-width: min(65vw, 1600px); --form-item-max-width--medium: min(50vw, 1400px); @@ -292,9 +306,9 @@ $semantics: ( --form-item-max-width--small: 100%; } - --form-label-font-weight: 500; - --form-input-padding: 0.5rem; - --form-input-border-radius: 6px; + --form-label-font-weight: 450; + --form-input-padding: 0.625rem; + --form-input-border-radius: 0.625rem; --form-input-glow: 0 0 0 10px transparent; --form-input-glow--focus: 0 0 0 5px var(--focus-input-glow); --pre-background: rgba(0, 0, 0, 0.05); diff --git a/war/src/main/scss/base/_style.scss b/war/src/main/scss/base/_style.scss index 71caf8b1e470..e586248d18a8 100644 --- a/war/src/main/scss/base/_style.scss +++ b/war/src/main/scss/base/_style.scss @@ -155,7 +155,7 @@ pre { background-color: var(--pre-background); color: var(--pre-color); font-family: var(--font-family-mono); - font-weight: 500; + font-weight: 400; line-height: 1.66; a { @@ -449,189 +449,6 @@ div.listview-jobs { display: block; } -/* ========================= build history ========================= */ -#buildHistory a:visited { - color: fuchsia; -} - -#buildHistory tr.no-wrap td.middle-align { - padding: 0; -} - -#buildHistory .desc { - position: relative; - padding: 0; - margin-top: 5px; - white-space: normal; - color: var(--text-color-secondary); - word-break: break-word; -} - -#buildHistory .build-row-cell { - position: relative; -} - -#buildHistory .build-rss-links { - display: flex; - justify-content: end; -} - -#buildHistory .build-rss-links a { - display: inline-flex; - align-items: center; - margin-right: 0.5rem; -} - -#buildHistory .build-rss-all-icon, -#buildHistory .build-rss-failed-icon { - margin-right: 0.25rem; - - svg { - width: 16px; - height: 16px; - } -} - -#buildHistoryPage { - position: relative; - - .build-search-row, - .build-search-no-results-row { - &:hover { - background: transparent !important; - } - - td { - padding: 0 8px 8px; - } - } - - .build-search-no-results-row { - border: none !important; - width: 100% !important; - background: transparent; - - td { - padding-top: 0 !important; - } - } -} - -#buildHistoryPageNav { - position: absolute; - right: -28px; - top: 96px; - border-radius: 6px; - background: var(--input-color); - border: 2px solid var(--input-border); - visibility: hidden; - z-index: 0; - opacity: 0; - transition: 0.2s ease; - - // Invisible pseudo element on the left so #buildHistoryPageNav - // doesn't disappear when moving cursor over gap - &::before { - content: ""; - position: absolute; - top: 0; - left: -8px; - bottom: 0; - width: 8px; - background: transparent; - z-index: -1; - } - - .buildHistoryPageNav__item { - position: relative; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - padding: 0 8px; - height: 30px; - cursor: pointer; - transition: opacity 0.2s ease; - - &:hover { - opacity: 0.5; - } - - &:active { - opacity: 0.25; - } - - &:not(:last-child) { - border-bottom: 2px solid rgba(0, 0, 0, 0.05); - } - - &-page-one-top { - width: 12px; - height: 2px; - background: currentColor; - margin: 2px 0; - border-radius: 2px; - } - - svg { - fill: currentColor; - width: 12px; - height: 12px; - } - } -} - -#buildHistoryPageNav.mouseOverSidePanel { - visibility: visible; - right: -32px; - opacity: 1; -} - -.build-row.model-link-active { - background: var(--light-grey) !important; -} - -.build-row-cell { - font-size: var(--font-size-xs); -} - -.build-row-cell .pane.build-name { - width: 25%; - font-weight: 500; - vertical-align: top; -} - -.build-row-cell .pane.build-details { - width: 50%; -} - -.build-row-cell .pane.build-controls { - width: 25%; - text-align: right; -} - -.build-row-cell .pane.build-details.block { - width: 100%; -} - -.pane.build-name a, -.pane.build-name a:visited { - color: var(--pane-link-color); - text-decoration: underline; -} - -.pane.build-details a, -.pane.build-details a:visited { - color: var(--pane-link-color--visited); - opacity: 0.6; - text-decoration: none; -} - -.pane.build-details a:hover { - opacity: 1; - text-decoration: underline; -} - /* ================ Element overflow calculation helper styles ================ */ .force-wrap, @@ -778,7 +595,7 @@ table.fingerprint-in-build td { } .radioBlock-container { - margin-bottom: calc(var(--section-padding) / 3); + margin-bottom: 0.875rem; &:last-of-type { margin-bottom: var(--section-padding); @@ -787,6 +604,7 @@ table.fingerprint-in-build td { .optionalBlock-container > .form-container, .radioBlock-container > .form-container, +.jenkins-radio__children, .dropdownList-container { position: relative; padding-left: 32px; @@ -872,7 +690,7 @@ table.progress-bar.red td.progress-bar-done { [data-symbol-animation] { animation: spin 1s linear infinite; - transform-origin: center; + transform-origin: 0 0; @media (prefers-reduced-motion) { animation-duration: 3s; @@ -1063,6 +881,18 @@ body.no-sticker #bottom-sticker { } /* see the Icon class for the definition of these CSS classes */ +.icon-xs, +svg.icon-xs { + width: 12px; + height: 12px; + vertical-align: middle; + + svg { + width: 12px; + height: 12px; + } +} + .icon-sm, svg.icon-sm { width: 16px; diff --git a/war/src/main/scss/base/_typography.scss b/war/src/main/scss/base/_typography.scss index 2b35478325fa..0dceec4624a6 100644 --- a/war/src/main/scss/base/_typography.scss +++ b/war/src/main/scss/base/_typography.scss @@ -49,7 +49,7 @@ h5, h6, .h6 { line-height: var(--line-height-heading); - font-weight: 650; + font-weight: 600; display: block; margin-top: 0; margin-bottom: var(--section-padding); @@ -62,12 +62,12 @@ h1, h2, .h2 { - font-size: 1.35rem; + font-size: 1.375rem; } h3, .h3 { - font-size: 1.2rem; + font-size: 1.1875rem; } h4, @@ -77,12 +77,12 @@ h4, h5, .h5 { - font-size: 0.8rem; + font-size: 0.8125rem; } h6, .h6 { - font-size: 0.6rem; + font-size: 0.625rem; } .jenkins-description { diff --git a/war/src/main/scss/components/_alert.scss b/war/src/main/scss/components/_alert.scss index 3397eeaab91d..16be23858b32 100644 --- a/war/src/main/scss/components/_alert.scss +++ b/war/src/main/scss/components/_alert.scss @@ -1,4 +1,5 @@ -.alert { +.alert, +.jenkins-alert { font-size: var(--font-size-sm); padding: 15px; margin-bottom: 20px; @@ -8,47 +9,47 @@ strong { font-weight: 500; } -} - -.alert a { - color: inherit; - text-decoration: underline; - &:hover, - &:focus, - &:active { + a { + color: inherit; text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } } -} -.alert-success { - color: var(--alert-success-text-color); - background-color: var(--alert-success-bg-color); - border-color: var(--alert-success-border-color); -} + &-success { + color: var(--alert-success-text-color); + background-color: var(--alert-success-bg-color); + border-color: var(--alert-success-border-color); + } -.alert-info { - color: var(--alert-info-text-color); - background-color: var(--alert-info-bg-color); - border-color: var(--alert-info-border-color); -} + &-info { + color: var(--alert-info-text-color); + background-color: var(--alert-info-bg-color); + border-color: var(--alert-info-border-color); + } -.alert-warning { - color: var(--alert-warning-text-color); - background-color: var(--alert-warning-bg-color); - border-color: var(--alert-warning-border-color); -} + &-warning { + color: var(--alert-warning-text-color); + background-color: var(--alert-warning-bg-color); + border-color: var(--alert-warning-border-color); -.alert-warning p { - color: var(--alert-warning-text-color); -} + p { + color: var(--alert-warning-text-color); + } + } -.alert-danger { - color: var(--alert-danger-text-color); - background-color: var(--alert-danger-bg-color); - border-color: var(--alert-danger-border-color); -} + &-danger { + color: var(--alert-danger-text-color); + background-color: var(--alert-danger-bg-color); + border-color: var(--alert-danger-border-color); -.alert-danger p { - color: var(--alert-danger-text-color); + p { + color: var(--alert-danger-text-color); + } + } } diff --git a/war/src/main/scss/components/_app-bar.scss b/war/src/main/scss/components/_app-bar.scss index 0cff962e7967..b1b30048ca70 100644 --- a/war/src/main/scss/components/_app-bar.scss +++ b/war/src/main/scss/components/_app-bar.scss @@ -9,7 +9,7 @@ justify-content: center; flex-direction: column; width: 100%; - min-height: 36px; + min-height: 2.25rem; } .jenkins-app-bar__controls { @@ -17,20 +17,14 @@ align-items: center; justify-content: center; margin-left: var(--section-padding); - min-height: 36px; - gap: 1rem; + min-height: 2.25rem; + gap: 0.75rem; .jenkins-search { min-width: 260px; } } - &--border { - margin-bottom: var(--section-padding); - padding-bottom: var(--section-padding); - border-bottom: 2px solid var(--panel-border-color); - } - &--sticky { position: sticky; top: 40px; @@ -69,6 +63,11 @@ margin: 0; font-size: 1.5rem; } + + &__subtitle { + color: var(--text-color-secondary); + margin-left: 0.5ch; + } } .jenkins-build-caption { diff --git a/war/src/main/scss/components/_badges.scss b/war/src/main/scss/components/_badges.scss index 0b4d2644cf19..7fc69376af34 100644 --- a/war/src/main/scss/components/_badges.scss +++ b/war/src/main/scss/components/_badges.scss @@ -37,26 +37,14 @@ align-items: center; justify-content: center; border-radius: 100px; - font-size: 0.7rem; + font-size: 0.6875rem; + font-weight: 400; min-height: 20px; min-width: 20px; padding: 0 0.4rem; - box-shadow: 0 1px 1px rgba(black, 0.1); - animation: animate-in-badge var(--elastic-transition) 0.1s both; - - &::after { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient(white, black); - border-radius: 100px; - mix-blend-mode: overlay; - opacity: 0.35; - } -} - -@keyframes animate-in-badge { - from { - transform: scale(0); - } + background: color-mix(in sRGB, var(--color) 85%, transparent); + box-shadow: inset 0 -1px 2px var(--color); + color: var(--white) !important; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(2.5px); } diff --git a/war/src/main/scss/components/_breadcrumbs.scss b/war/src/main/scss/components/_breadcrumbs.scss index dda12847140e..f50677846430 100644 --- a/war/src/main/scss/components/_breadcrumbs.scss +++ b/war/src/main/scss/components/_breadcrumbs.scss @@ -26,7 +26,7 @@ justify-content: center; color: var(--text-color); font-weight: 500; - font-size: 0.85rem; + font-size: 0.875rem; padding: 0.2rem 0.4rem; & > a { diff --git a/war/src/main/scss/components/_buttons.scss b/war/src/main/scss/components/_buttons.scss index 5108aca2c6eb..4812949af07c 100644 --- a/war/src/main/scss/components/_buttons.scss +++ b/war/src/main/scss/components/_buttons.scss @@ -14,15 +14,14 @@ border: none; outline: none; margin: 0; - padding: 0.5rem 0.85rem; - font-size: 0.8125rem; - font-weight: 500; + padding: 0.5rem 0.9rem; + font-size: 0.875rem; text-decoration: none !important; background: transparent; color: var(--text-color) !important; border-radius: 0.66rem; cursor: pointer; - min-height: 36px; + min-height: 2.25rem; white-space: nowrap; gap: 1ch; transition: var(--standard-transition); @@ -32,8 +31,8 @@ } svg { - width: 1.1rem; - height: 1.1rem; + width: 1.125rem; + height: 1.125rem; } &:disabled { @@ -45,7 +44,6 @@ .jenkins-button--primary { color: var(--button-color--primary) !important; - font-weight: 600; &::before { background: var(--accent-color) !important; diff --git a/war/src/main/scss/components/_cards.scss b/war/src/main/scss/components/_cards.scss new file mode 100644 index 000000000000..be86bb390337 --- /dev/null +++ b/war/src/main/scss/components/_cards.scss @@ -0,0 +1,97 @@ +$card-padding: 1rem; + +.jenkins-card { + position: relative; + border-radius: 1rem; + margin-bottom: calc(var(--section-padding) / 2); + background: var(--card-background); + + &__title { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 $card-padding; + height: 50px; + font-size: var(--font-size-sm) !important; + font-weight: 500; + width: 100%; + z-index: 1; + } + + &__controls { + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + margin-right: -0.2rem; + } + + &:not(:hover) { + .jenkins-card__unveil { + color: var(--text-color-secondary) !important; + } + } + + &:hover { + .jenkins-card__reveal { + color: var(--text-color) !important; + } + } + + &__content { + display: flex; + flex-direction: column; + padding: 0 $card-padding $card-padding; + color: var(--text-color-secondary); + + &:empty { + display: none; + } + } + + &::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + border: var(--card-border-width) solid var(--card-border-color); + z-index: 1; + pointer-events: none; + } + + .jenkins-card__reveal { + display: flex; + align-items: center; + justify-content: center; + margin-block: -0.5rem; + min-height: 0; + padding: 0; + width: 26px; + height: 26px; + border-radius: 0.33rem; + color: var(--text-color-secondary) !important; + transition: + scale var(--standard-transition), + opacity var(--standard-transition); + + svg { + width: 1rem; + height: 1rem; + transition: color var(--standard-transition); + } + + &::before, + &::after { + opacity: 0; + } + + &:hover { + opacity: 0.75; + } + + &:active { + scale: 95%; + opacity: 0.5; + } + } +} diff --git a/war/src/main/scss/components/_dialogs.scss b/war/src/main/scss/components/_dialogs.scss index a53831e5acb1..3de2d0c883a0 100644 --- a/war/src/main/scss/components/_dialogs.scss +++ b/war/src/main/scss/components/_dialogs.scss @@ -19,8 +19,8 @@ $jenkins-dialog-padding: 1.3rem; } &__title { - font-size: 1.1rem; - font-weight: 600; + font-size: 1.125rem; + font-weight: 500; padding: 0 $jenkins-dialog-padding; color: var(--text-color); } @@ -58,7 +58,7 @@ $jenkins-dialog-padding: 1.3rem; &__subtitle { font-size: 1rem; - font-weight: 600; + font-weight: 500; color: var(--text-color-secondary); padding: 0; margin: 0 0 1rem; diff --git a/war/src/main/scss/components/_dropdowns.scss b/war/src/main/scss/components/_dropdowns.scss index 586a07fc5adf..7268773f8fab 100644 --- a/war/src/main/scss/components/_dropdowns.scss +++ b/war/src/main/scss/components/_dropdowns.scss @@ -92,7 +92,7 @@ $dropdown-padding: 0.4rem; color: var(--text-color-secondary) !important; margin: $dropdown-padding 0.55rem; font-size: 0.8125rem; - font-weight: 600; + font-weight: 500; opacity: 0.8; &:not(:first-of-type) { @@ -133,7 +133,7 @@ $dropdown-padding: 0.4rem; margin: 0; padding: $dropdown-padding 1.75rem $dropdown-padding 0.6rem; font-size: 0.8125rem; - font-weight: 500; + font-weight: 450; text-decoration: none !important; background: transparent; color: var(--text-color) !important; @@ -254,3 +254,18 @@ $dropdown-padding: 0.4rem; border-radius: 50%; } } + +.jenkins-jumplist-link { + appearance: none; + border: none; + background: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + svg { + width: 1.25rem; + height: 1.25rem; + } +} diff --git a/war/src/main/scss/components/_icons.scss b/war/src/main/scss/components/_icons.scss index 5a823400871e..c35d0613487c 100644 --- a/war/src/main/scss/components/_icons.scss +++ b/war/src/main/scss/components/_icons.scss @@ -127,7 +127,5 @@ } .pulse-animation { - svg { - animation: pulse-animation 2s ease infinite; - } + animation: pulse-animation 2s ease infinite; } diff --git a/war/src/main/scss/components/_index.scss b/war/src/main/scss/components/_index.scss index 3048d06ae2c5..f3167bd091f9 100644 --- a/war/src/main/scss/components/_index.scss +++ b/war/src/main/scss/components/_index.scss @@ -2,8 +2,9 @@ @use "alert"; @use "badges"; @use "breadcrumbs"; -@use "buttons-deprecated"; @use "buttons"; +@use "buttons-deprecated"; +@use "cards"; @use "content-blocks"; @use "dialogs"; @use "dropdowns"; diff --git a/war/src/main/scss/components/_progress-bar.scss b/war/src/main/scss/components/_progress-bar.scss index 1e1faf37d630..2a92b570a226 100644 --- a/war/src/main/scss/components/_progress-bar.scss +++ b/war/src/main/scss/components/_progress-bar.scss @@ -10,7 +10,6 @@ var(--text-color-secondary) 25%, transparent ); - margin-top: 2px; display: block; opacity: 1 !important; diff --git a/war/src/main/scss/components/_section.scss b/war/src/main/scss/components/_section.scss index a637deb6181e..9a53a7238339 100644 --- a/war/src/main/scss/components/_section.scss +++ b/war/src/main/scss/components/_section.scss @@ -30,8 +30,8 @@ .jenkins-section__title { margin: 0 0 var(--section-padding) 0; - font-size: 1.1rem; - font-weight: 600; + font-size: 1rem; + font-weight: 500; } .jenkins-section__items { @@ -106,17 +106,17 @@ } dt { - font-size: 0.925rem; - font-weight: 600; + font-size: 0.9375rem; + font-weight: 500; margin: 0.1rem 0 0.2rem; color: var(--text-color); } dd { color: var(--text-color-secondary); - font-weight: 500; + font-weight: 450; line-height: 1.6; margin: 0 0.66rem 0 0; - font-size: 0.925rem; + font-size: 0.9375rem; } } diff --git a/war/src/main/scss/components/_side-panel-tasks.scss b/war/src/main/scss/components/_side-panel-tasks.scss index df5a362315d0..1ae9168ce235 100644 --- a/war/src/main/scss/components/_side-panel-tasks.scss +++ b/war/src/main/scss/components/_side-panel-tasks.scss @@ -8,7 +8,7 @@ $background-outset: 0.7rem; display: flex; flex-direction: column; margin: var(--section-padding); - gap: 5px; + gap: 0.125rem; /* stylelint-disable-next-line media-query-no-invalid */ @media (min-width: breakpoints.$tablet-breakpoint) { @@ -63,11 +63,11 @@ $background-outset: 0.7rem; align-items: center; justify-content: flex-start; padding: 0.55rem $background-outset; - gap: 0.75rem; + gap: 0.65rem; width: 100%; cursor: pointer; - font-weight: 500 !important; - font-size: 0.9rem; + font-weight: 450 !important; + font-size: 0.875rem; color: var(--text-color) !important; background: transparent; outline: none; @@ -101,11 +101,11 @@ $background-outset: 0.7rem; } &--active { - font-weight: 600 !important; + font-weight: 500 !important; cursor: default; svg * { - stroke-width: 38px; + stroke-width: 35px; } &::before { diff --git a/war/src/main/scss/components/_side-panel-widgets.scss b/war/src/main/scss/components/_side-panel-widgets.scss index bd6715cdc586..6771da8d1d92 100644 --- a/war/src/main/scss/components/_side-panel-widgets.scss +++ b/war/src/main/scss/components/_side-panel-widgets.scss @@ -49,6 +49,11 @@ } } +#side-panel .pane-header-details { + font-weight: normal; + font-size: var(--font-size-xs); +} + #side-panel .pane-content { font-size: var(--font-size-xs); } @@ -87,164 +92,59 @@ /** * Executors */ -#executors th.pane { - text-align: left; -} - -/** - * Build history - */ -.build-row { - padding: 3px 4px; -} - -.build-row.model-link-active { - background: var(--very-light-grey) !important; -} - -.build-row-cell { - font-size: var(--font-size-xs); -} - -.build-row-cell .pane.build-name { - width: 32%; - font-weight: 500; - vertical-align: top; -} - -.build-row-cell .pane.build-details { - width: 50%; -} - -.build-row-cell .pane.build-controls { - width: 18%; - text-align: right; -} - -.build-row-cell .pane.build-details.block { - width: 100%; -} - -.build-row.multi-line .build-row-cell .pane.build-name.block { - width: 100%; -} - -.build-row-cell .pane.build-controls.block { - width: 100%; -} - -.build-row-cell .pane.build-name .build-icon, -.build-row-cell .pane.build-name .display-name { - display: inline-block; -} - -.build-row-cell .pane.build-name .build-icon { - position: absolute; - margin-top: 2px; - z-index: 1; -} - -.build-row-cell .build-stop { - display: inline-block; - width: 30%; -} - -.build-row-cell .build-badge { - display: inline-block; - text-align: right; - width: 70%; - padding: 2px 0; -} - -.build-row-cell .build-badge > span { - display: inline-block; - max-width: 256px; - padding: 0 1px; - overflow: hidden; -} - -.build-row-cell .build-badge > span + span { - margin: 0 0 0 2px !important; -} - -@media (width >= 1170px) { - .build-row-cell .build-badge > span { - max-width: 296px; +#executors { + th.pane { + text-align: left; } -} -.build-row .build-name-controls .pane.build-name, -.build-row .build-details-controls .pane.build-details { - width: 70%; -} + .pane-header { + align-items: center; + } -.build-row .build-row-cell .pane, -#side-panel .build-row .build-row-cell .pane { - padding: 0 2px; /* Sync changes with func expandControlsTo50Percent in hudson-behavior.js */ - display: inline-block; - overflow: hidden; -} + .computer-caption { + display: flex; + gap: 5px; + padding: 0.5rem 1rem; -.build-row.multi-line .build-row-cell .block { - display: block; - overflow: auto; -} - -.build-row.multi-line .build-row-cell .indent-multiline { - margin-top: 5px; -} + & > div { + margin-left: auto; + align-self: end; + } + } -.build-row.multi-line .build-row-cell .left-bar { - position: absolute; - top: 31px; - bottom: 10px; - left: 17px; - border-left: 1px solid var(--medium-grey); -} + .executors-cell { + padding: 0 1rem; + } -.build-row-cell .pane.build-name .display-name { - margin-left: 20px; - word-break: break-all; -} + .executors-collapsed { + padding: 0 1rem 0.5rem; + } -.build-row-cell .indent-multiline { - padding-left: 20px !important; /* Sync changes with func expandControlsTo50Percent in hudson-behavior.js */ -} + .executor-row { + display: flex; + padding: 0 0 0.5rem; + align-items: center; + } -.build-row.overflow-checked .build-row-cell { - visibility: visible; -} + .executor-type { + width: 16px; + margin-right: 1ch; + } -.jenkins-pane { - &__information { - text-align: center; - line-height: 80px; - background-color: var(--panel-header-bg-color); - margin-top: 10px; - font-weight: 600; - border-radius: var(--form-input-border-radius); + .executor-cell { + width: 100%; } - .build-row { - transition: opacity 0.2s ease; + .executor-cell-table .pane { + margin: 0 !important; + padding: 0 !important; - &-cell { - padding: 4px 8px; + & > div { + margin-right: 15px; } } - &--loading .build-row { - opacity: 0.5; - } -} - -.jenkins-pane__header--build-history { - display: grid; - grid-template-columns: auto 1fr auto; - font-weight: 500 !important; - - .jenkins-table__cell--tight { - width: auto; - margin-right: 1rem; + .executor-stop { + width: 16px; } } diff --git a/war/src/main/scss/components/_spinner.scss b/war/src/main/scss/components/_spinner.scss index a538e89c355c..af5732ca8320 100644 --- a/war/src/main/scss/components/_spinner.scss +++ b/war/src/main/scss/components/_spinner.scss @@ -2,8 +2,8 @@ position: relative; display: inline-flex; align-items: center; - font-size: 0.85rem; - font-weight: 600; + font-size: 0.875rem; + font-weight: 500; margin: 0; &::before, diff --git a/war/src/main/scss/components/_table.scss b/war/src/main/scss/components/_table.scss index 5ac54d12d927..f247407a8f3b 100644 --- a/war/src/main/scss/components/_table.scss +++ b/war/src/main/scss/components/_table.scss @@ -3,14 +3,25 @@ .jenkins-table { --table-padding: 0.55rem; + position: relative; width: 100%; background: var(--table-background); - border-radius: calc(var(--table-border-radius) + 2px); - border: 5px solid var(--table-background); - border-bottom-width: 3px; + border-radius: calc(var(--table-border-radius) + 4px); + border: 4px solid var(--table-background); + border-bottom-width: 2px; border-spacing: 0 2px; + background-clip: padding-box; margin-bottom: var(--section-padding); + &::before { + content: ""; + position: absolute; + inset: -4px -4px -2px; + border: var(--card-border-width) solid var(--table-border-color); + border-radius: inherit; + pointer-events: none; + } + * { -webkit-border-horizontal-spacing: 0; -webkit-border-vertical-spacing: 0; @@ -21,11 +32,11 @@ & > th { color: var(--table-header-foreground); text-align: left; - padding-top: calc((var(--table-padding) * 1.7) - 7.5px); - padding-bottom: calc((var(--table-padding) * 1.7) - 2.5px); + padding-top: calc(var(--table-padding) * 0.9); + padding-bottom: calc((var(--table-padding) * 0.9) + 2px); padding-left: 1.6rem; - font-weight: 600; - font-size: 0.85rem; + font-weight: 500; + font-size: 0.875rem; &[align="center"] { text-align: center; @@ -69,7 +80,6 @@ background: var(--table-body-background); vertical-align: middle; padding: var(--table-padding) 0 var(--table-padding) 1.6rem; - font-weight: 500; height: 3rem; &:first-of-type { @@ -154,6 +164,18 @@ } } + .jenkins-button { + margin: -10px 0; + padding: 0.5rem 0.75rem; + min-height: 1.75rem; + + // Increase the size of symbols compared to regular buttons + svg { + width: 1.5rem !important; + height: 1.5rem !important; + } + } + &__button, &__icon { svg, @@ -181,6 +203,15 @@ height: 1.3rem !important; } } + + .jenkins-button { + padding: 0.4rem 0.6rem; + + svg { + width: 1.3rem !important; + height: 1.3rem !important; + } + } } &--auto-width { @@ -204,6 +235,15 @@ height: 1rem !important; } } + + .jenkins-button { + padding: 0.3rem 0.5rem; + + svg { + width: 1rem !important; + height: 1rem !important; + } + } } &__button, diff --git a/war/src/main/scss/components/_tabs.scss b/war/src/main/scss/components/_tabs.scss index 565d62e2f81a..0e54d9646a99 100644 --- a/war/src/main/scss/components/_tabs.scss +++ b/war/src/main/scss/components/_tabs.scss @@ -3,14 +3,24 @@ } .tabBar { + position: relative; display: inline-flex; align-items: center; flex-wrap: wrap; background: var(--tabs-background); border-radius: var(--tabs-border-radius); - padding: 2.5px; + padding: 2px; margin-bottom: var(--section-padding); + &::before { + content: ""; + position: absolute; + inset: 0; + border: var(--card-border-width) solid var(--tabs-border-color); + border-radius: inherit; + pointer-events: none; + } + .tab { float: left; } @@ -23,13 +33,13 @@ justify-content: center; min-width: 3rem; text-decoration: none; - margin: 2.5px; + margin: 2px; padding: 0.4rem 1.2rem; border-radius: 100px; background: var(--tabs-item-background); color: var(--tabs-item-foreground); - font-weight: 600; - font-size: 0.85rem; + font-weight: 500; + font-size: 0.875rem; transition: var(--standard-transition); cursor: pointer; diff --git a/war/src/main/scss/components/_tooltips.scss b/war/src/main/scss/components/_tooltips.scss index 80a030b8c9c3..d864f98bd941 100644 --- a/war/src/main/scss/components/_tooltips.scss +++ b/war/src/main/scss/components/_tooltips.scss @@ -3,7 +3,7 @@ padding: 0.45rem 0.8rem; border-radius: 0.66rem; box-shadow: var(--tooltip-box-shadow); - font-weight: 550; + font-weight: 500; font-size: 0.75rem; line-height: 1.6; max-width: min(50vw, 1000px) !important; diff --git a/war/src/main/scss/form/_checkbox.scss b/war/src/main/scss/form/_checkbox.scss index 8d23d9e3977d..d9cd5f18c951 100644 --- a/war/src/main/scss/form/_checkbox.scss +++ b/war/src/main/scss/form/_checkbox.scss @@ -140,6 +140,7 @@ box-shadow: 0 0 0 10px transparent, inset 0 0 0 2px var(--input-border); + background: var(--input-color); } &::after { diff --git a/war/src/main/scss/form/_codemirror.scss b/war/src/main/scss/form/_codemirror.scss index 67520192c0e5..a34165e8ba57 100644 --- a/war/src/main/scss/form/_codemirror.scss +++ b/war/src/main/scss/form/_codemirror.scss @@ -42,6 +42,16 @@ .CodeMirror-selected { background-color: var(--selection-color) !important; } + + .CodeMirror-lines { + padding: var(--form-input-padding); + } + + .CodeMirror-gutter-text { + padding: var(--form-input-padding) calc(var(--form-input-padding) * 0.5) + var(--form-input-padding) var(--form-input-padding); + color: var(--text-color-secondary); + } } .jenkins-codemirror-resizer { diff --git a/war/src/main/scss/form/_layout.scss b/war/src/main/scss/form/_layout.scss index 0214d596c112..8983245875e2 100644 --- a/war/src/main/scss/form/_layout.scss +++ b/war/src/main/scss/form/_layout.scss @@ -45,7 +45,7 @@ font-weight: var(--form-label-font-weight); margin-top: 0; margin-bottom: 0.5rem; - padding-inline: 0 0; + padding-inline: 0; } .jenkins-form-description { @@ -93,7 +93,7 @@ background: var(--text-color); color: transparent; mask-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='262px' height='482px' viewBox='0 0 262 482' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M69.9217381,109.622622 L69.9347383,109.509526 L69.9838606,109.158807 L70.0463523,108.766768 C70.2927563,107.286617 70.7336,105.509397 71.3554148,103.600821 C73.4488365,97.1753443 76.974163,91.0239622 81.9750299,85.3670497 C84.5508811,82.453282 87.4853512,79.7201177 90.8001215,77.1662697 C100.73686,69.5111158 114.003751,65.6584363 131.476234,65.4354882 C133.731084,65.4096204 135.996257,65.4453704 138.232058,65.5407008 L139.478148,65.6000218 C140.992957,65.679511 142.536904,65.7906863 144.04662,65.9294921 L145.210323,66.0424992 C145.886163,66.1116384 146.60564,66.1925244 147.313561,66.2795642 L148.414194,66.420922 L149.377674,66.5545415 L150.375686,66.7050246 L151.398071,66.8714663 L152.049055,66.9841252 L152.731845,67.1083183 L153.642724,67.2845248 L154.549432,67.4714994 C155.036905,67.5756456 155.574038,67.6978797 156.090422,67.8232778 L156.917997,68.0311744 C158.666031,68.484436 160.136789,68.9816723 161.141416,69.4394425 C170.146558,73.5618171 177.044937,78.1235016 182.224862,83.4639805 C188.857993,90.3027083 191.999176,97.8308192 191.999176,107.499034 C191.999176,119.466025 188.796962,127.918279 181.370643,136.250976 C181.093904,136.561492 180.816698,136.867422 180.538309,137.169526 L180.089688,137.652102 L179.736214,138.026258 L179.002023,138.784568 L178.579132,139.213445 L178.134441,139.655368 L177.707007,140.071619 L177.021999,140.726828 L176.298991,141.400731 L175.788688,141.867317 L175.318979,142.286805 L174.54297,142.970055 L173.990783,143.446079 L173.448394,143.905722 L172.625948,144.588916 L171.799887,145.262466 L170.811596,146.050172 L169.864089,146.789794 L169.222049,147.28309 L168.549302,147.793886 L167.493519,148.583956 L166.782836,149.108414 L165.65246,149.931951 L164.484527,150.770371 L163.266904,151.632649 L161.555386,152.827104 L160.682666,153.429152 L159.298283,154.375903 L157.864408,155.346877 L155.333592,157.040637 L153.750968,158.088827 L151.524574,159.551762 L148.580787,161.468911 L144.800696,163.908618 C105.775292,188.991858 87.997951,218.376069 87.997951,257.047472 L87.997951,283.286279 L88.0016284,283.748414 C88.3121212,301.66652 103.883328,316 122.998363,316 C142.328557,316 157.998775,301.353568 157.998775,283.286279 L157.998775,257.047472 L158.001383,256.432449 C158.064604,248.820487 159.375708,243.378146 162.208358,238.456376 C165.925109,231.998468 172.860638,225.357973 183.865352,218.206756 L186.747913,216.348707 L189.667785,214.457346 L192.470459,212.628123 L195.158954,210.857753 L197.739048,209.141153 L199.612339,207.881962 L201.424999,206.651521 L203.184757,205.444126 L204.893987,204.257626 L206.014818,203.471391 L207.63679,202.320739 L208.708154,201.551674 L209.751591,200.795045 L210.776652,200.043916 L211.783987,199.29767 L212.774236,198.555696 L213.748026,197.81739 L214.705977,197.082156 L215.983535,196.08689 L217.035512,195.253661 L217.94273,194.524595 L218.836751,193.796023 L219.718133,193.067399 L220.587422,192.33819 L221.445157,191.607874 L222.291869,190.87594 L223.128081,190.141889 L224.349696,189.048475 L225.176062,188.294328 L225.979581,187.549096 L227.15568,186.436436 L227.953672,185.666523 L228.73078,184.904536 L229.501513,184.136777 L230.633743,182.986693 L231.403964,182.189294 L232.15663,181.397931 L232.905389,180.598844 L233.650817,179.791592 L234.393505,178.975725 L235.134055,178.150785 C253.063009,158.033696 262,134.778085 262,107.499034 C262,81.4013251 252.315995,58.3143983 234.08637,39.5197386 C222.737894,27.8195118 209.350637,18.7642961 192.813373,11.0878005 L191.857565,10.6472715 C176.144833,3.48753386 154.016249,-0.255558118 130.568693,0.0135609187 C97.385459,0.43694412 69.212387,9.00946009 46.2762841,26.6791815 C39.5211589,31.8836333 33.4271839,37.5663685 28.0119541,43.6920026 C17.038175,56.105392 9.16079603,69.8997291 4.39056696,84.5413054 C2.80350137,89.4125897 1.62984552,94.1870463 0.865737279,98.781619 L0.782196688,99.2931802 L0.614828552,100.383438 L0.559658337,100.765066 L0.426811303,101.749189 L0.318522027,102.638537 L0.226704214,103.487005 L0.123646908,104.617718 L0.0652556787,105.43714 L0.0342145373,105.994277 C-0.81250758,124.122996 14.1596716,139.394013 33.4714881,140.181654 L33.9656162,140.198367 L34.5332314,140.20872 C53.0445487,140.409453 68.5022596,127.170521 69.8964399,109.989591 L69.9217381,109.622622 Z' fill='currentColor' fill-rule='nonzero'%3E%3C/path%3E%3Ccircle fill='currentColor' cx='123' cy='434' r='48'%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); - mask-size: 50% 50%; + mask-size: 45% 45%; mask-position: center; mask-repeat: no-repeat; } @@ -102,7 +102,7 @@ content: ""; position: absolute; inset: 0; - background: var(--text-color); + background: var(--text-color-secondary); opacity: 0.1; border-radius: inherit; transition: var(--standard-transition); @@ -112,7 +112,8 @@ content: ""; position: absolute; inset: 0; - border: 1px solid rgba(125, 125, 125, 0.35); + border: 1px solid + color-mix(in sRGB, var(--text-color-secondary), transparent); box-shadow: var(--form-input-glow); border-radius: inherit; opacity: 0.1; diff --git a/war/src/main/scss/form/_radio.scss b/war/src/main/scss/form/_radio.scss index 68f95df38f4b..a673e86d6daf 100644 --- a/war/src/main/scss/form/_radio.scss +++ b/war/src/main/scss/form/_radio.scss @@ -1,3 +1,11 @@ +$jenkins-radio-size: 1.375rem; +$jenkins-radio-border-size: 0.125rem; +$jenkins-radio-border-hover-size: 0.3125rem; +$jenkins-radio-border-active-size: 0.5rem; +$jenkins-radio-border-checked-size: 0.4rem; +$jenkins-radio-glow-size: 0.625rem; +$jenkins-radio-glow-active-size: 0.3125rem; + .jenkins-radio-help-wrapper { display: flex; align-items: center; @@ -5,69 +13,59 @@ } .jenkins-radio { - margin-top: 3px; - &:not(:last-of-type) { - margin-bottom: calc(var(--section-padding) / 3); + margin-bottom: 0.875rem; } &__input { position: absolute; opacity: 0; - margin-top: 10px; + margin-top: 0.625rem; - &:hover { - & + label { - &::before { + &:not(:checked) { + &:hover { + & + label::before { box-shadow: - 0 0 0 10px transparent, - inset 0 0 0 5px var(--input-border-hover); + 0 0 0 $jenkins-radio-glow-size transparent, + inset 0 0 0 $jenkins-radio-border-hover-size + var(--input-border-hover); } } - } - &:not(:disabled) { + &:focus-visible, &:focus, &:active { - & + label { - &::before { - transition: box-shadow var(--elastic-transition); - box-shadow: - 0 0 0 5px var(--focus-input-glow), - inset 0 0 0 5px var(--focus-input-border); - } - } - } - - &:checked { - &:focus, - &:active { - & + label { - &::before { - box-shadow: - 0 0 0 5px var(--focus-input-glow), - inset 0 0 0 8px var(--focus-input-border); - } - } + & + label::before { + box-shadow: + 0 0 0 $jenkins-radio-glow-active-size var(--focus-input-glow), + inset 0 0 0 $jenkins-radio-border-active-size + var(--focus-input-border); } } } &:checked { & + label { - &:active, - &:focus { - &::before { - box-shadow: - 0 0 0 5px var(--focus-input-glow), - inset 0 0 0 12px var(--focus-input-border); - } + cursor: default; + + &::before { + box-shadow: + 0 0 0 $jenkins-radio-glow-size transparent, + inset 0 0 0 $jenkins-radio-border-checked-size + var(--focus-input-border); } } - & + label { - &::before { - box-shadow: inset 0 0 0 8px var(--focus-input-border); + &:not(:disabled) { + &:focus-visible, + &:focus, + &:active { + & + label::before { + box-shadow: + 0 0 0 $jenkins-radio-glow-active-size var(--focus-input-glow), + inset 0 0 0 $jenkins-radio-border-active-size + var(--focus-input-border); + } } } } @@ -85,11 +83,9 @@ &__label { position: relative; - top: -5px; display: inline-block; - margin-top: 10px; margin-bottom: 0; - padding: 0 0 5px 32px; + padding: 0 0 0 2rem; cursor: pointer; font-weight: var(--form-label-font-weight); // remove 300ms pause on mobile @@ -101,54 +97,24 @@ position: absolute; top: 0; left: 0; - width: 22px; - height: 22px; + width: $jenkins-radio-size; + height: $jenkins-radio-size; border-radius: 50%; - background: transparent; + background: var(--input-color); box-shadow: - 0 0 0 10px transparent, - inset 0 0 0 2px var(--input-border); + 0 0 0 $jenkins-radio-glow-size transparent, + inset 0 0 0 $jenkins-radio-border-size var(--input-border); transition: box-shadow var(--standard-transition); } } &__description { - margin: 0 0 0 32px; + margin: 0 0 0 2rem; color: var(--text-color-secondary); line-height: 1.66; } - &__children { - position: relative; - margin-top: 10px; - opacity: 0; - padding-left: 32px; - transition: var(--standard-transition); - visibility: hidden; - max-height: 0; - - &::after { - content: ""; - position: absolute; - top: 0; - left: 10px; - bottom: 0; - width: 2px; - background: var(--input-border); - border-radius: 2px; - transition: var(--standard-transition); - } - - &:focus-within { - &::after { - background: var(--focus-input-border); - } - } - } - - &__input:checked + &__label + &__children { - visibility: visible; - opacity: 1; - max-height: none; + &__input:not(:checked) + &__label + &__children { + display: none; } } diff --git a/war/src/main/scss/form/_reorderable-list.scss b/war/src/main/scss/form/_reorderable-list.scss index c19152975d2e..7cd726556df1 100644 --- a/war/src/main/scss/form/_reorderable-list.scss +++ b/war/src/main/scss/form/_reorderable-list.scss @@ -7,6 +7,14 @@ border-radius: 10px; margin-bottom: 1rem; margin-top: 1rem; + transition: + opacity 0.2s ease-in, + max-height 0.2s ease-in; +} + +.repeated-chunk.fade-in, +.repeated-chunk.fade-out { + opacity: 0; } .repeated-chunk .show-if-last { diff --git a/war/src/main/scss/form/_search-bar.scss b/war/src/main/scss/form/_search-bar.scss index 81f7450570e4..ed552e85c987 100644 --- a/war/src/main/scss/form/_search-bar.scss +++ b/war/src/main/scss/form/_search-bar.scss @@ -21,7 +21,6 @@ 0 0 0 2px transparent, 0 0 0 12px transparent; transition: var(--standard-transition); - font-weight: 500; height: var(--search-bar-height); &::placeholder { @@ -142,7 +141,7 @@ justify-content: center; transition: 0.25s ease; text-align: center; - font-size: 0.8em; + font-size: 0.6875rem; font-weight: 500; line-height: 1; color: var(--text-color-secondary); @@ -257,7 +256,7 @@ align-items: center; gap: 0.7rem; padding: 0.5rem 0.7rem; - font-size: 0.85rem; + font-size: 0.875rem; border-radius: 10px; color: var(--text-color); font-weight: 500; diff --git a/war/src/main/scss/form/_select.scss b/war/src/main/scss/form/_select.scss index acdedfc17728..e8b2b2973b94 100644 --- a/war/src/main/scss/form/_select.scss +++ b/war/src/main/scss/form/_select.scss @@ -21,10 +21,10 @@ appearance: none; display: block; border: 2px solid var(--input-border); - padding: 8px; + padding: var(--form-input-padding); width: 100% !important; // TODO remove important after https://github.com/jenkinsci/credentials-plugin/pull/255 max-width: 100% !important; // TODO remove important after https://github.com/jenkinsci/credentials-plugin/pull/255 - border-radius: 6px; + border-radius: var(--form-input-border-radius); box-shadow: 0 0 0 10px transparent; transition: var(--standard-transition); min-height: 38px; diff --git a/war/src/main/scss/pages/_about.scss b/war/src/main/scss/pages/_about.scss index 08be61408749..b39fc84e4cd9 100644 --- a/war/src/main/scss/pages/_about.scss +++ b/war/src/main/scss/pages/_about.scss @@ -124,13 +124,13 @@ } .app-about-version { - font-weight: 600; + font-weight: 500; color: var(--text-color-secondary); margin: 0; } .app-about-paragraph { - font-size: 1.1rem; + font-size: 1.125rem; margin-bottom: var(--section-padding); font-weight: 500; } diff --git a/war/src/main/scss/pages/_build.scss b/war/src/main/scss/pages/_build.scss new file mode 100644 index 000000000000..6bcf404773fa --- /dev/null +++ b/war/src/main/scss/pages/_build.scss @@ -0,0 +1,7 @@ +.build-caption-progress-container { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + gap: 10px; +} diff --git a/war/src/main/scss/pages/_dashboard.scss b/war/src/main/scss/pages/_dashboard.scss index cb029a9861a9..3ace60fac309 100644 --- a/war/src/main/scss/pages/_dashboard.scss +++ b/war/src/main/scss/pages/_dashboard.scss @@ -121,7 +121,7 @@ $min-button-size: 36px; &__label { color: var(--link-color); - font-weight: 600; + font-weight: 500; margin: 0; font-size: 1rem; } diff --git a/war/src/main/scss/pages/_icon-legend.scss b/war/src/main/scss/pages/_icon-legend.scss index 02cb37cff584..9d62a5c084aa 100644 --- a/war/src/main/scss/pages/_icon-legend.scss +++ b/war/src/main/scss/pages/_icon-legend.scss @@ -23,7 +23,7 @@ dd { margin: 0; padding: 0; - font-size: 0.95rem; + font-size: 0.9375rem; font-weight: 500; line-height: 1.6; } diff --git a/war/src/main/scss/pages/_index.scss b/war/src/main/scss/pages/_index.scss index 8e019eebb7b0..c8e20e272271 100644 --- a/war/src/main/scss/pages/_index.scss +++ b/war/src/main/scss/pages/_index.scss @@ -1,6 +1,8 @@ @use "about"; +@use "build"; @use "dashboard"; @use "icon-legend"; +@use "job"; @use "manage-jenkins"; @use "plugin-manager"; @use "setupWizardFirstUser"; diff --git a/war/src/main/scss/pages/_job.scss b/war/src/main/scss/pages/_job.scss new file mode 100644 index 000000000000..3b982fcffcf4 --- /dev/null +++ b/war/src/main/scss/pages/_job.scss @@ -0,0 +1,184 @@ +@use "../abstracts/mixins"; + +#buildHistoryPage { + margin: 10px 0 10px 10px; + + .jenkins-search { + margin-inline: -0.25rem; + margin-bottom: 5px; + } +} + +.app-builds-container { + transition: opacity var(--standard-transition); + + &__items { + margin-bottom: -0.5rem; + } + + &__placeholder { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 3rem; + animation: fade-in-builds-placeholder var(--standard-transition); + + @keyframes fade-in-builds-placeholder { + from { + opacity: 0; + } + } + } + + &__heading { + display: flex; + font-size: 0.75rem; + color: var(--text-color-secondary); + margin-top: 10px; + font-weight: 450; + margin-bottom: 4px; + } + + &__controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin: 0 -0.35rem; + margin-top: 1rem; + margin-bottom: -0.5rem; + + .jenkins-button { + padding: 10px; + + svg { + transition: translate var(--standard-transition); + } + + &:first-of-type { + justify-content: start; + + &:hover { + translate: -2px 0; + + svg { + translate: -4px 0; + } + } + } + + &:last-of-type { + justify-content: end; + + &:hover { + translate: 2px 0; + + svg { + translate: 4px 0; + } + } + } + } + + .app-builds-container__button--disabled { + color: var(--text-color-secondary) !important; + opacity: 0.25; + pointer-events: none; + } + } + + &--loading { + opacity: 0.4; + filter: blur(0.5px); + } +} + +.app-builds-container__item { + @include mixins.item(); + + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.5rem 0.65rem; + padding: 0 0 0.25rem; + margin: 0 -0.5rem; + font-size: 0.8125rem !important; + min-height: 2rem; + + &__icon { + display: inline-flex; + justify-content: center; + padding: 0 0 0 0.5rem; + margin-top: 0.385rem; + + svg { + width: 1.25rem; + height: 1.25rem; + } + } + + .app-builds-container__item__inner { + display: flex; + align-items: stretch; + flex-wrap: wrap; + + &__link { + display: flex; + color: var(--text-color); + gap: 0.5rem; + text-decoration: none; + font-weight: 450; + flex-grow: 1; + padding: 0.45rem 0 0; + + .app-builds-container__item__time { + color: var(--text-color-secondary); + } + } + + &__controls { + display: flex; + align-items: center; + justify-content: start; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.3rem; + } + } + + &--not-interactable { + cursor: default; + + &::before, + &::after { + display: none; + } + + .app-builds-container__item__description { + margin-bottom: 0; + } + } + + .jenkins-jumplist-link { + margin-top: 0.2rem; + padding-right: 0.8rem; + } + + &__description { + color: var(--text-color-secondary); + padding-left: 2.25rem; + margin-top: -2px; + grid-column: 1 / span 2; + + &::before { + content: ""; + position: absolute; + left: 17px; + top: 34px; + bottom: 6px; + width: 2px; + background: var(--text-color-secondary); + border-radius: 10px; + opacity: 0.3; + } + } +} diff --git a/war/src/main/scss/pages/_manage-jenkins.scss b/war/src/main/scss/pages/_manage-jenkins.scss index 7b4f364cf779..706fe2e49a09 100644 --- a/war/src/main/scss/pages/_manage-jenkins.scss +++ b/war/src/main/scss/pages/_manage-jenkins.scss @@ -34,32 +34,29 @@ content: ": "; } -.manage-messages { - .alert:last-of-type { +.manage-messages .alert, +.manage-messages .jenkins-alert { + &:last-of-type { margin-bottom: 30px; } -} -.manage-messages .alert a { - text-decoration: underline; -} + a { + text-decoration: underline; + } -.manage-messages .alert a:hover { - text-decoration: underline; -} + form { + position: relative; + float: right; + margin: -6px 0 0 !important; + display: flex; + gap: 0.5rem; -.manage-messages .alert form { - position: relative; - float: right; - margin: -6px 0 0 !important; - display: flex; - gap: 0.5rem; + & > div { + display: contents; + } - & > div { - display: contents; + span { + margin: 0 0 0 4px !important; + } } } - -.manage-messages .alert form span { - margin: 0 0 0 4px !important; -} diff --git a/war/src/main/scss/simple-page.scss b/war/src/main/scss/simple-page.scss index c80f6e1303d7..08bb4f1493e2 100644 --- a/war/src/main/scss/simple-page.scss +++ b/war/src/main/scss/simple-page.scss @@ -36,14 +36,21 @@ } .simple-page .safe-restarting { + font-size: var(--font-size-sm); + padding: 15px; + border-radius: 10px; text-align: center; - border-color: var(--alert-success-border-color); - background-color: var(--alert-success-bg-color); - border-width: 2px; - border-radius: 2px; - border-style: solid; margin: 5% auto auto; - padding: 5px; + background-color: var(--alert-success-bg-color); + border: 1px solid var(--alert-success-border-color); + + * { + color: var(--alert-success-text-color) !important; + } + + strong { + font-weight: 500; + } } .simple-page .safe-restarting > p { @@ -97,6 +104,7 @@ border-color: var(--danger-color, #c4000a); } -.simple-page .alert { +.simple-page .alert, +.simple-page .jenkins-alert { color: var(--danger-color, #c4000a); } diff --git a/war/src/main/webapp/WEB-INF/web.xml b/war/src/main/webapp/WEB-INF/web.xml index 29e9194decd2..4ef6db9e3fb5 100644 --- a/war/src/main/webapp/WEB-INF/web.xml +++ b/war/src/main/webapp/WEB-INF/web.xml @@ -66,8 +66,8 @@ THE SOFTWARE. true - compression-filter - org.kohsuke.stapler.compression.CompressionFilter + uncaught-exception-filter + org.kohsuke.stapler.UncaughtExceptionFilter true @@ -149,7 +149,7 @@ THE SOFTWARE. /* - compression-filter + uncaught-exception-filter /* diff --git a/war/src/main/webapp/help/system-config/computerRetentionCheckInterval.html b/war/src/main/webapp/help/system-config/computerRetentionCheckInterval.html new file mode 100644 index 000000000000..1c85b796d431 --- /dev/null +++ b/war/src/main/webapp/help/system-config/computerRetentionCheckInterval.html @@ -0,0 +1,13 @@ +
+ Configure the check interval (in seconds) of computer retention strategies. + Retention strategies are configured in the individual agent configuration + under "Availability". Administrators can trade elevated CPU usage from + frequent agent checks for responsiveness to capacity needs. +

+ For example, if you run your build agents on short-lived VMs that boot fast, + you may want to scale up new capacity as soon as builds queue up and scale + VMs down as soon as no more work is available to cut delays for users and + costs of keeping idle VMs alive. Be sure to adjust the delays in the + respective retention strategies accordingly as well. +

+
diff --git a/war/src/main/webapp/scripts/behavior.js b/war/src/main/webapp/scripts/behavior.js index cb4d01821bed..6ab59c627c9b 100644 --- a/war/src/main/webapp/scripts/behavior.js +++ b/war/src/main/webapp/scripts/behavior.js @@ -184,7 +184,6 @@ Behaviour.start(); function findElementsBySelector(startNode, selector, includeSelf) { if (includeSelf) { var isSelfOrChild = function (c) { - // eslint-disable-next-line no-constant-condition while (true) { if (startNode == c) { return true; diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js index ca068e55e272..6f877b5f8332 100644 --- a/war/src/main/webapp/scripts/hudson-behavior.js +++ b/war/src/main/webapp/scripts/hudson-behavior.js @@ -360,7 +360,6 @@ function findNearBy(e, name) { function locate(iterator, e) { // keep finding elements until we find the good match - // eslint-disable-next-line no-constant-condition while (true) { e = iterator(e, name); if (e == null) { @@ -593,7 +592,7 @@ function parseHtml(html) { * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#direct_and_indirect_eval */ function geval(script) { - eval(script); + (0, eval)(script); } /** @@ -693,6 +692,7 @@ function registerValidator(e) { var url = e.targetUrl(); try { FormChecker.delayedCheck(url, method, e.targetElement); + // eslint-disable-next-line no-unused-vars } catch (x) { // this happens if the checkUrl refers to a non-existing element. // don't let this kill off the entire JavaScript @@ -903,57 +903,79 @@ function escapeHTML(html) { } /** - * Wraps a