diff --git a/war/.babelrc b/.babelrc similarity index 100% rename from war/.babelrc rename to .babelrc diff --git a/.editorconfig b/.editorconfig index af8f0cf310a8..049d90619a70 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ root = true -[*.{js, scss, css, hbs}] +[*.{js, scss, css, hbs, svg}] indent_style = space indent_size = 2 trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes index 63ca2fc710ef..85a9c64aa951 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,4 +39,4 @@ # Yarn # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored -/war/.yarn/plugins/** binary +/.yarn/plugins/** binary 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 7b25e51631b5..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.426" - 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/release-drafter.yml b/.github/release-drafter.yml index 18da01b85864..35f3eaeaa698 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -7,7 +7,7 @@ tag-template: jenkins-$NEXT_MINOR_VERSION template: | _This is an automatically generated changelog draft for Jenkins weekly releases. - See https://www.jenkins.io/changelog/#v$NEXT_MINOR_VERSION for the official changelog for this release, or https://www.jenkins.io/changelog-old/#v$NEXT_MINOR_VERSION for releases older than around 7 months._ + See https://www.jenkins.io/changelog/$NEXT_MINOR_VERSION/ for the official changelog for this release._ $CHANGES diff --git a/.github/renovate.json b/.github/renovate.json index 8c3c4ad17cea..90c5a18d9f6d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,63 +1,210 @@ { "$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.ee9:jetty-ee9-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 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": [ + "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/announce-lts-rc.yml b/.github/workflows/announce-lts-rc.yml index b544abd78f3e..2f3c0de4e584 100644 --- a/.github/workflows/announce-lts-rc.yml +++ b/.github/workflows/announce-lts-rc.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Post on Discourse - uses: roots/discourse-topic-github-release-action@fc9e50fa1a1ce6255ba4d03f104382845b79ad5f # v1.0.0 + uses: roots/discourse-topic-github-release-action@c30dc233349b7c6f24f52fb1c659cc64f13b5474 # v1.0.1 with: discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }} discourse-base-url: https://community.jenkins.io/ diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 70391d2e42dd..f7032327b8df 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -24,7 +24,7 @@ jobs: # Drafts your next Release notes as Pull Requests are merged into "master" - name: Generate GitHub Release Draft id: release-drafter - uses: release-drafter/release-drafter@v5 + uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Generates a YAML changelog file using https://github.com/jenkinsci/jenkins-core-changelog-generator @@ -35,7 +35,7 @@ jobs: env: GITHUB_AUTH: github-actions:${{ secrets.GITHUB_TOKEN }} - name: Upload Changelog YAML - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: changelog.yaml path: changelog.yaml @@ -44,12 +44,15 @@ jobs: runs-on: ubuntu-latest if: github.repository_owner == 'jenkinsci' steps: - - uses: tibdex/github-app-token@v1 + - uses: tibdex/github-app-token@v2 id: generate-token with: app_id: ${{ secrets.JENKINS_CHANGELOG_UPDATER_APP_ID }} + installation_retrieval_mode: repository + installation_retrieval_payload: jenkins-infra/jenkins.io private_key: ${{ secrets.JENKINS_CHANGELOG_UPDATER_PRIVATE_KEY }} - repository: jenkins-infra/jenkins.io + repositories: >- + ["jenkins.io"] - name: Check out uses: actions/checkout@v4 with: @@ -64,4 +67,8 @@ jobs: GIT_COMMITTER_EMAIL: <86592549+jenkins-infra-changelog-generator[bot]@users.noreply.github.com> run: | wget --quiet https://raw.githubusercontent.com/jenkinsci/core-changelog-generator/master/generate-weekly-changelog.sh + # Create a Python virtual environment for pip install + # See https://github.com/jenkinsci/core-changelog-generator/issues/37 + python3 -m venv venv + source venv/bin/activate bash generate-weekly-changelog.sh diff --git a/.github/workflows/label-conflicting-pr.yml b/.github/workflows/label-conflicting-pr.yml index 00e1b80988b3..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@v2.1.0 + uses: eps1lon/actions-label-merge-conflict@v3.0.2 with: dirtyLabel: "unresolved-merge-conflict" repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/label-lts-prs.yaml b/.github/workflows/label-lts-prs.yaml index e947bd078f0f..4e63806f751d 100644 --- a/.github/workflows/label-lts-prs.yaml +++ b/.github/workflows/label-lts-prs.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Check if PR targets LTS branch if: startsWith(github.event.pull_request.base.ref, 'stable-') - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/publish-release-artifact.yml b/.github/workflows/publish-release-artifact.yml index 222b2b5d2ffb..3bd728e9632b 100644 --- a/.github/workflows/publish-release-artifact.yml +++ b/.github/workflows/publish-release-artifact.yml @@ -13,13 +13,14 @@ jobs: outputs: project-version: ${{ steps.set-version.outputs.project-version }} is-lts: ${{ steps.set-version.outputs.is-lts }} + is-rc: ${{ steps.set-version.outputs.is-rc }} steps: - uses: actions/checkout@v4 - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: "temurin" - java-version: 11 + java-version: 17 cache: "maven" - name: Set version id: set-version @@ -34,16 +35,23 @@ jobs: is_lts=false fi - echo "Version is $version, is_lts: $is_lts" + is_rc=false + if [[ ${version} == *"-SNAPSHOT" ]]; then + is_rc=true + fi + + echo "Version is $version, is_lts: $is_lts, is_rc: $is_rc" echo "is-lts=${is_lts}" >> $GITHUB_OUTPUT echo "project-version=$version" >> $GITHUB_OUTPUT + echo "is-rc=${is_rc}" >> $GITHUB_OUTPUT war: permissions: contents: write # to upload release asset (softprops/action-gh-release) runs-on: ubuntu-latest needs: determine-version + if: ${{ needs.determine-version.outputs.is-rc == 'false' }} steps: - name: Fetch war id: fetch-war @@ -65,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@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -76,6 +84,7 @@ jobs: runs-on: ubuntu-latest needs: determine-version + if: ${{ needs.determine-version.outputs.is-rc == 'false' }} steps: - name: Fetch Deb id: fetch-deb @@ -99,7 +108,7 @@ jobs: - name: Upload Release Asset id: upload-deb if: always() - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -110,6 +119,7 @@ jobs: runs-on: ubuntu-latest needs: determine-version + if: ${{ needs.determine-version.outputs.is-rc == 'false' }} steps: - name: Fetch RPM id: fetch-rpm @@ -134,7 +144,7 @@ jobs: - name: Upload Release Asset id: upload-rpm if: always() - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -145,6 +155,7 @@ jobs: runs-on: ubuntu-latest needs: determine-version + if: ${{ needs.determine-version.outputs.is-rc == 'false' }} steps: - name: Fetch MSI id: fetch-msi @@ -169,7 +180,7 @@ jobs: - name: Upload Release Asset id: upload-msi if: always() - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -180,6 +191,7 @@ jobs: runs-on: ubuntu-latest needs: determine-version + if: ${{ needs.determine-version.outputs.is-rc == 'false' }} steps: - name: Fetch suse rpm id: fetch-suse-rpm @@ -204,7 +216,7 @@ jobs: - name: Upload Release Asset id: upload-suse-rpm if: always() - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/run-since-updater.yml b/.github/workflows/run-since-updater.yml new file mode 100644 index 000000000000..6241acdc2dc4 --- /dev/null +++ b/.github/workflows/run-since-updater.yml @@ -0,0 +1,41 @@ +name: Run update-since-todo.py + +on: + schedule: + - cron: "0 16 * * THU" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + since_updater: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'jenkinsci' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Run update-since-todo.py + run: | + body=$(./update-since-todo.py) + + { + echo 'PROGRESS<> $GITHUB_OUTPUT + id: run_script + shell: bash + - name: Create Pull Request + uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Fill in since annotations + title: Fill in since annotations + body: ${{ steps.run_script.outputs.PROGRESS }} + base: master + labels: skip-changelog + branch: actions/update-since-todo + delete-branch: true diff --git a/.gitignore b/.gitignore index 021201b717a3..41747ca0cd23 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,25 @@ jenkins_*.changes *.pkg *.zip push-build.sh -war/node_modules/ -war/yarn-error.log +node_modules/ +yarn-error.log .java-version .checkstyle + +/rebel.xml +junit.xml + +# Yarn +# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/sdks +!.yarn/versions + +# Node +node/ +node_modules/ + +# Generated JavaScript Bundles +war/src/main/webapp/jsbundles/ diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile index 01d953e840e7..58d3fa8a87f4 100644 --- a/.gitpod/Dockerfile +++ b/.gitpod/Dockerfile @@ -1,6 +1,6 @@ FROM gitpod/workspace-full -ARG MAVEN_VERSION=3.9.5 +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..68f564fff79d 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -29,9 +29,9 @@ - - - + + + diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 1f36364094f1..30a03ea18d31 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -1,7 +1,7 @@ - + io.jenkins.tools.incrementals git-changelist-maven-extension - 1.7 + 1.8 diff --git a/.mvn/jvm.config b/.mvn/jvm.config index f0106d148540..1d72cb7d8d93 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -1 +1 @@ --Xmx1100m +-Xmx1400m diff --git a/war/.prettierignore b/.prettierignore similarity index 72% rename from war/.prettierignore rename to .prettierignore index 8047dce8ba4f..72610f04b419 100644 --- a/war/.prettierignore +++ b/.prettierignore @@ -7,17 +7,15 @@ node/ .git -.yarnrc.yml - # libraries / external deps / generated files src/main/js/plugin-setup-wizard/bootstrap-detached.js -src/main/webapp/scripts/yui -src/main/webapp/jsbundles/ +war/src/main/webapp/scripts/yui +war/src/main/webapp/jsbundles/ src/main/scss/_bootstrap.scss # test files that we don't need formatted -../test/src/test/resources -../test/jmh-report.json +test/src/test/resources +test/jmh-report.json # doesn't work, see https://github.com/prettier/prettier/issues/5340 *.hbs @@ -25,4 +23,4 @@ src/main/scss/_bootstrap.scss .yarn # Incorrectly flagging forwarding slashes in regex -../.github/renovate.json +.github/renovate.json diff --git a/war/.stylelintrc.js b/.stylelintrc.js similarity index 100% rename from war/.stylelintrc.js rename to .stylelintrc.js diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 000000000000..5004f58b1bfd --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,2 @@ +enableGlobalCache: false +nodeLinker: node-modules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e6dbffdda9d..98650aa99de6 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 or 17. + - 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). + - Apache Maven 3.9.6 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,33 @@ 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 + +> [!TIP] +> If you already have Node.js installed, you do not need to change your path. Start using Yarn by enabling [Corepack](https://yarnpkg.com/corepack) with `corepack enable`, if it isn't already; this will add the `yarn` binary to your path. + +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/node:$PWD/node/node_modules/corepack/shims:$PATH +``` + +Then you can run Yarn with e.g. + +```sh +yarn +``` + ### Building frontend assets To work on the `war` module frontend assets, two processes are needed at the same time: @@ -55,13 +74,13 @@ 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: +Open another terminal and start a [webpack](https://webpack.js.org/) dev server, after [optionally adding Node and Yarn to your path](#running-the-yarn-frontend-build): ```sh -cd war; yarn start +yarn start ``` ### Gitpod @@ -86,11 +105,22 @@ 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 [optionally adding Node and Yarn to your path](#running-the-yarn-frontend-build), run: + +```sh +yarn lint +``` + +To fix frontend issues, after [optionally adding Node and Yarn to your path](#running-the-yarn-frontend-build), run: + +```sh +yarn lint:fix ``` ## Testing changes @@ -111,14 +141,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 2913ec74060b..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() /* @@ -118,10 +118,12 @@ axes.values().combinations { } mavenOptions.add(0, "-Dsurefire.excludesFile=${excludesFile}") } - realtimeJUnit(healthScaleFactor: 20.0, testResults: '*/target/surefire-reports/*.xml') { - infra.runMaven(mavenOptions, jdk) - if (isUnix()) { - sh 'git add . && git diff --exit-code HEAD' + withChecks(name: 'Tests', includeStage: true) { + realtimeJUnit(healthScaleFactor: 20.0, testResults: '*/target/surefire-reports/*.xml') { + infra.runMaven(mavenOptions, jdk) + if (isUnix()) { + sh 'git add . && git diff --exit-code HEAD' + } } } } @@ -219,10 +221,13 @@ athAxes.values().combinations { // Just to be safe deleteDir() checkout scm - infra.withArtifactCachingProxy { - sh "bash ath.sh ${jdk} ${browser}" + + withChecks(name: 'Tests', includeStage: true) { + infra.withArtifactCachingProxy { + sh "bash ath.sh ${jdk} ${browser}" + } + junit testResults: 'target/ath-reports/TEST-*.xml', testDataPublishers: [[$class: 'AttachmentPublisher']] } - junit testResults: 'target/ath-reports/TEST-*.xml', testDataPublishers: [[$class: 'AttachmentPublisher']] /* * Currently disabled, as the fact that this is a manually created subset will confuse Launchable, * which expects this to be a full build. When we implement subsetting, this can be re-enabled using diff --git a/README.md b/README.md index 50f7007ee0ec..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. @@ -70,4 +71,4 @@ See [adopters](https://www.jenkins.io/project/adopters/) for the list of Jenkins # License -Jenkins is **licensed** under the **[MIT License](https://github.com/jenkinsci/jenkins/blob/master/LICENSE.txt)**. +Jenkins is **licensed** under the **[MIT License](LICENSE.txt)**. diff --git a/ath.sh b/ath.sh index db84a79fff2a..e25bf4b945e8 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=5740.vd30f30408987 +export ATH_VERSION=6081.v29b_ce3c2771c if [[ $# -eq 0 ]]; then export JDK=17 @@ -26,11 +26,15 @@ fi mkdir -p target/ath-reports chmod a+rwx target/ath-reports +# obtain the groupId to grant to access the docker socket to run tests needing docker +dockergid=$(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ubuntu:noble stat -c %g /var/run/docker.sock) + exec docker run --rm \ --env JDK \ --env ATH_VERSION \ --env BROWSER \ --shm-size 2g `# avoid selenium.WebDriverException exceptions like 'Failed to decode response from marionette' and webdriver closed` \ + --group-add ${dockergid} \ --volume "$(pwd)"/war/target/jenkins.war:/jenkins.war:ro \ --volume /var/run/docker.sock:/var/run/docker.sock:rw \ --volume "$(pwd)"/target/ath-reports:/reports:rw \ diff --git a/bom/pom.xml b/bom/pom.xml index 595b6b3f383a..9413a5957ef0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -22,7 +22,7 @@ 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. --> - + 4.0.0 @@ -38,9 +38,8 @@ THE SOFTWARE. The module contains dependencies that are used by a specific Jenkins version - 9.6 - 2.0.9 - 1822.v120278426e1c + 2.0.0-M2 + 1928.v9115fe47607f 2.4.21 @@ -53,18 +52,25 @@ THE SOFTWARE. pom import + + org.slf4j + slf4j-bom + 2.0.16 + pom + import + org.springframework spring-framework-bom - 5.3.30 + 6.2.0 pom import - + org.springframework.security spring-security-bom - 5.8.8 + 6.3.4 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 - 32.1.3-jre + 33.3.1-jre @@ -103,7 +109,7 @@ THE SOFTWARE. com.thoughtworks.xstream xstream - 1.4.20 + 1.4.21 commons-beanutils @@ -113,22 +119,17 @@ THE SOFTWARE. commons-codec commons-codec - 1.16.0 + 1.17.1 commons-collections commons-collections 3.2.2 - - commons-fileupload - commons-fileupload - 1.5 - commons-io commons-io - 2.15.0 + 2.17.0 commons-jelly @@ -150,10 +151,15 @@ THE SOFTWARE. jenkins-stapler-support 1.1 + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + jakarta.servlet.jsp.jstl jakarta.servlet.jsp.jstl-api - 1.2.7 + 2.0.0 jaxen @@ -163,7 +169,7 @@ THE SOFTWARE. net.java.dev.jna jna - 5.13.0 + 5.15.0 net.java.sezpoz @@ -183,12 +189,47 @@ THE SOFTWARE. org.apache.ant ant - 1.10.14 + 1.10.15 org.apache.commons commons-compress - 1.24.0 + 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 @@ -196,9 +237,9 @@ THE SOFTWARE. ${groovy.version} - org.connectbot.jbcrypt + org.connectbot jbcrypt - 1.0.0 + 1.0.2 @@ -209,7 +250,7 @@ THE SOFTWARE. org.jenkins-ci annotation-indexer - 1.17 + 1.18 org.jenkins-ci @@ -219,27 +260,27 @@ THE SOFTWARE. org.jenkins-ci crypto-util - 1.9 + 1.10 org.jenkins-ci memory-monitor - 1.12 + 1.13 org.jenkins-ci symbol-annotation - 1.24 + 1.25 org.jenkins-ci task-reactor - 1.8 + 1.9 org.jenkins-ci version-number - 1.11 + 1.12 org.jenkins-ci.main @@ -254,7 +295,7 @@ THE SOFTWARE. org.jvnet.hudson commons-jelly-tags-define - 1.1-jenkins-20230713 + 1.1-jenkins-20241115 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-8 org.kohsuke.stapler @@ -311,62 +352,22 @@ 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} + 9.7.1 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.2 + 1.3.4 provided diff --git a/cli/pom.xml b/cli/pom.xml index fa8971e32ac7..2473629d23cc 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -1,5 +1,5 @@ - + 4.0.0 @@ -15,7 +15,7 @@ https://github.com/jenkinsci/jenkins - 2.11.0 + 2.14.0 @@ -65,7 +65,7 @@ org.glassfish.tyrus.bundles tyrus-standalone-client-jdk - 2.1.3 + 2.2.0 true @@ -119,7 +119,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.0 diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index 9aee43f6f9a0..910331b3ee9a 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -32,6 +32,7 @@ import jakarta.websocket.ClientEndpointConfig; import jakarta.websocket.Endpoint; import jakarta.websocket.EndpointConfig; +import jakarta.websocket.HandshakeResponse; import jakarta.websocket.Session; import java.io.DataInputStream; import java.io.File; @@ -58,14 +59,13 @@ import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; -import org.apache.commons.lang.StringUtils; import org.glassfish.tyrus.client.ClientManager; import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.client.SslEngineConfigurator; +import org.glassfish.tyrus.client.exception.DeploymentHandshakeException; import org.glassfish.tyrus.container.jdk.client.JdkClientContainer; /** @@ -134,6 +134,7 @@ public static int _main(String[] _args) throws Exception { String tokenEnv = System.getenv("JENKINS_API_TOKEN"); boolean strictHostKey = false; + boolean noCertificateCheck = false; while (!args.isEmpty()) { String head = args.get(0); @@ -179,17 +180,7 @@ public static int _main(String[] _args) throws Exception { } if (head.equals("-noCertificateCheck")) { LOGGER.info("Skipping HTTPS certificate checks altogether. Note that this is not secure at all."); - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, new TrustManager[]{new NoCheckTrustManager()}, new SecureRandom()); - HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); - // bypass host name check, too. - HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { - @Override - @SuppressFBWarnings(value = "WEAK_HOSTNAME_VERIFIER", justification = "User set parameter to skip verifier.") - public boolean verify(String s, SSLSession sslSession) { - return true; - } - }); + noCertificateCheck = true; args = args.subList(1, args.size()); continue; } @@ -255,9 +246,9 @@ public boolean verify(String s, SSLSession sslSession) { if (auth == null && bearer == null) { // -auth option not set - if (StringUtils.isNotBlank(userIdEnv) && StringUtils.isNotBlank(tokenEnv)) { - auth = StringUtils.defaultString(userIdEnv).concat(":").concat(StringUtils.defaultString(tokenEnv)); - } else if (StringUtils.isNotBlank(userIdEnv) || StringUtils.isNotBlank(tokenEnv)) { + if ((userIdEnv != null && !userIdEnv.isBlank()) && (tokenEnv != null && !tokenEnv.isBlank())) { + auth = userIdEnv.concat(":").concat(tokenEnv); + } else if ((userIdEnv != null && !userIdEnv.isBlank()) || (tokenEnv != null && !tokenEnv.isBlank())) { printUsage(Messages.CLI_BadAuth()); return -1; } // Otherwise, none credentials were set @@ -305,7 +296,7 @@ public boolean verify(String s, SSLSession sslSession) { LOGGER.warning("Warning: -user ignored unless using -ssh"); } - CLIConnectionFactory factory = new CLIConnectionFactory(); + CLIConnectionFactory factory = new CLIConnectionFactory().noCertificateCheck(noCertificateCheck); String userInfo = new URL(url).getUserInfo(); if (userInfo != null) { factory = factory.basicAuth(userInfo); @@ -351,17 +342,44 @@ public void onOpen(Session session, EndpointConfig config) {} } class Authenticator extends ClientEndpointConfig.Configurator { + HandshakeResponse hr; @Override public void beforeRequest(Map> headers) { if (factory.authorization != null) { headers.put("Authorization", List.of(factory.authorization)); } } + @Override + public void afterResponse(HandshakeResponse hr) { + this.hr = hr; + } } + var authenticator = new Authenticator(); ClientManager client = ClientManager.createClient(JdkClientContainer.class.getName()); // ~ ContainerProvider.getWebSocketContainer() client.getProperties().put(ClientProperties.REDIRECT_ENABLED, true); // https://tyrus-project.github.io/documentation/1.13.1/index/tyrus-proprietary-config.html#d0e1775 - Session session = client.connectToServer(new CLIEndpoint(), ClientEndpointConfig.Builder.create().configurator(new Authenticator()).build(), URI.create(url.replaceFirst("^http", "ws") + "cli/ws")); + if (factory.noCertificateCheck) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] {new NoCheckTrustManager()}, new SecureRandom()); + SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator(sslContext); + sslEngineConfigurator.setHostnameVerifier((s, sslSession) -> true); + client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator); + } + Session session; + try { + session = client.connectToServer(new CLIEndpoint(), ClientEndpointConfig.Builder.create().configurator(authenticator).build(), URI.create(url.replaceFirst("^http", "ws") + "cli/ws")); + } catch (DeploymentHandshakeException x) { + System.err.println("CLI handshake failed with status code " + x.getHttpStatusCode()); + if (authenticator.hr != null) { + for (var entry : authenticator.hr.getHeaders().entrySet()) { + // org.glassfish.tyrus.core.Utils.parseHeaderValue improperly splits values like Date at commas, so undo that: + System.err.println(entry.getKey() + ": " + String.join(", ", entry.getValue())); + } + // UpgradeResponse.getReasonPhrase is useless since Jetty generates it from the code, + // and the body is not accessible at all. + } + return 15; // compare CLICommand.main + } PlainCLIProtocol.Output out = new PlainCLIProtocol.Output() { @Override public void send(byte[] data) throws IOException { @@ -386,8 +404,15 @@ public void close() throws IOException { } } - private static int plainHttpConnection(String url, List args, CLIConnectionFactory factory) throws IOException, InterruptedException { + private static int plainHttpConnection(String url, List args, CLIConnectionFactory factory) + throws GeneralSecurityException, IOException, InterruptedException { LOGGER.log(FINE, "Trying to connect to {0} via plain protocol over HTTP", url); + if (factory.noCertificateCheck) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] {new NoCheckTrustManager()}, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier((s, sslSession) -> true); + } FullDuplexHttpStream streams = new FullDuplexHttpStream(new URL(url), "cli?remoting=false", factory.authorization); try (ClientSideImpl connection = new ClientSideImpl(new PlainCLIProtocol.FramedOutput(streams.getOutputStream()))) { connection.start(args); diff --git a/cli/src/main/java/hudson/cli/CLIConnectionFactory.java b/cli/src/main/java/hudson/cli/CLIConnectionFactory.java index 9b4295b07cb3..615f8bfa35cb 100644 --- a/cli/src/main/java/hudson/cli/CLIConnectionFactory.java +++ b/cli/src/main/java/hudson/cli/CLIConnectionFactory.java @@ -10,6 +10,7 @@ */ public class CLIConnectionFactory { String authorization; + boolean noCertificateCheck; /** * For CLI connection that goes through HTTP, sometimes you need @@ -21,6 +22,16 @@ public CLIConnectionFactory authorization(String value) { return this; } + /** + * Skip TLS certificate and hostname verification checks. + * + * @since 2.444 + */ + public CLIConnectionFactory noCertificateCheck(boolean value) { + this.noCertificateCheck = value; + return this; + } + /** * Convenience method to call {@link #authorization} with the HTTP basic authentication. * Currently unused. diff --git a/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java index 5a1167c4fdc5..41d7a719dd09 100644 --- a/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java +++ b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java @@ -21,7 +21,7 @@ class FlightRecorderInputStream extends InputStream { * Size (in bytes) of the flight recorder ring buffer used for debugging remoting issues. * @since 2.41 */ - static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024 * 1024); + static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024); private final InputStream source; private ByteArrayRingBuffer recorder = new ByteArrayRingBuffer(BUFFER_SIZE); diff --git a/cli/src/main/resources/hudson/cli/client/Messages_sv_SE.properties b/cli/src/main/resources/hudson/cli/client/Messages_sv_SE.properties new file mode 100644 index 000000000000..34db1671108b --- /dev/null +++ b/cli/src/main/resources/hudson/cli/client/Messages_sv_SE.properties @@ -0,0 +1,26 @@ +CLI.Usage=Jenkins kommandoradsgränssnitt\n\ + Användning: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ + Alternativ:\n\ + \ -s URL : serverns webbadress (standard är miljövariabeln JENKINS_URL)\n\ + \ -webSocket : anslut med WebSocket (standard; fungerar med de flesta omvända proxys; kräver Jetty)\n\ + \ -http : använd ett par HTTP(S)-anslutningar istället för WebSocket\n\ + \ -ssh : använd SSH-protokoll istället för WebSocket (kräver -user; SSH-porten måste vara öppen på servern)\n\ + \ -i KEY : privat SSH-nyckel som används för autentisering (används med -ssh)\n\ + \ -noCertificateCheck : kringgå kontroll av HTTPS-certifikat helt och hållet. Används varsamt\n\ + \ -noKeyAuth : försök inte läsa privata SSH-autentiseringsnyckeln. Ligger i konflikt med -i\n\ + \ -user : specificera användare (används med -ssh; måste ha registrerats med en offentlig nyckel)\n\ + \ -strictHostKey : begär strikt kontroll av värdnyckel (används med -ssh)\n\ + \ -logger FINE : aktivera detaljerad loggning från klienten\n\ + \ -auth [ USER:SECRET | @FILE ] : specificera användare och antingen lösenord eller API-token (eller läs in båda från en fil);\n\ + \ används med -http.\n\ + \ Det rekommenderas att bifoga inloggningsinformation via fil.\n\ + \ Se https://www.jenkins.io/redirect/cli-http-connection-mode för mer information och alternativ.\n\ + \ -bearer [ TOKEN | @FILE ] : specificera autentisering med en ägartoken (eller läs in token från en fil);\n\ + \ används med -http. Kan inte användas samtidigt med -auth.\n\ + \ Det rekommenderas att bifoga inloggningsinformation via fil.\n\ + \n\ + Tillgängliga kommandon beror på servern. Kör kommandot ''help'' för att \ + se listan. +CLI.NoURL=Varken -s eller miljövariabeln JENKINS_URL har specificerats. +CLI.NoSuchFileExists=Det finns ingen sådan fil: {0} +CLI.BadAuth=Miljövariablerna JENKINS_USER_ID och JENKINS_API_TOKEN bör anges eller lämnas tomma. diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java index 8d960e87a461..99586768b256 100644 --- a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java +++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java @@ -42,7 +42,7 @@ private void assertKeyPairNotNull(File file, String password) throws IOException } /** - key command: ssh-keygen -f dsa-password -t dsa -b 1024 -m PEM -p password + key command: ssh-keygen -f dsa-password -t dsa -b 1024 -m PEM -P password */ @Test public void loadKeyDSAPassword() throws IOException, GeneralSecurityException { @@ -61,7 +61,7 @@ public void loadKeyRSA() throws IOException, GeneralSecurityException { } /** - key command: ssh-keygen -f rsa-password -t rsa -b 1024 -m PEM -p password + key command: ssh-keygen -f rsa-password -t rsa -b 1024 -m PEM -P password */ @Test public void loadKeyRSAPassword() throws IOException, GeneralSecurityException { @@ -80,7 +80,7 @@ public void loadKeyOpenSSH() throws IOException, GeneralSecurityException { } /** - key command: ssh-keygen -f openssh-unsupported -t rsa -b 1024 -m PKCS8 -p password + key command: ssh-keygen -f openssh-unsupported -t rsa -b 1024 -m PKCS8 -P password */ @Test public void loadKeyOpenSSHPKCS8() throws IOException, GeneralSecurityException { diff --git a/core/pom.xml b/core/pom.xml index 45539865962a..9bff5e5ad0b2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -23,7 +23,7 @@ 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. --> - + 4.0.0 @@ -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 @@ -219,6 +217,20 @@ THE SOFTWARE. jakarta.servlet.jsp.jstl jakarta.servlet.jsp.jstl-api + + + jakarta.el + jakarta.el-api + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + jaxen @@ -276,13 +288,28 @@ THE SOFTWARE. org.apache.commons commons-compress + + + + org.apache.commons + commons-lang3 + + + + + org.apache.commons + commons-fileupload2-core + + + org.apache.commons + commons-fileupload2-jakarta-servlet5 org.codehaus.groovy groovy-all - org.connectbot.jbcrypt + org.connectbot jbcrypt @@ -402,26 +429,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 @@ -434,6 +441,10 @@ THE SOFTWARE. org.springframework.security spring-security-web + + io.micrometer + micrometer-observation + org.springframework spring-jcl @@ -452,7 +463,6 @@ THE SOFTWARE. jakarta.servlet jakarta.servlet-api - 4.0.4 provided @@ -517,6 +527,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 c16fc93e1aa9..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; @@ -86,11 +87,6 @@ public class ClassicPluginStrategy implements PluginStrategy { private final PluginManager pluginManager; - /** - * All the plugins eventually delegate this classloader to load core, servlet APIs, and SE runtime. - */ - private final MaskingClassLoader coreClassLoader = new MaskingClassLoader(getClass().getClassLoader()); - public ClassicPluginStrategy(PluginManager pluginManager) { this.pluginManager = pluginManager; } @@ -235,20 +231,16 @@ private static Manifest loadLinkedManifest(File archive) throws IOException { fix(atts, optionalDependencies); - // Register global classpath mask. This is useful for hiding JavaEE APIs that you might see from the container, - // such as database plugin for JPA support. The Mask-Classes attribute is insufficient because those classes - // also need to be masked by all the other plugins that depend on the database plugin. - String masked = atts.getValue("Global-Mask-Classes"); - if (masked != null) { - for (String pkg : masked.trim().split("[ \t\r\n]+")) - coreClassLoader.add(pkg); - } - - ClassLoader dependencyLoader = new DependencyClassLoader(coreClassLoader, archive, Util.join(dependencies, optionalDependencies), pluginManager); + ClassLoader dependencyLoader = new DependencyClassLoader( + getClass().getClassLoader(), archive, Util.join(dependencies, optionalDependencies), pluginManager); 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) { @@ -260,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); } } @@ -276,27 +268,44 @@ 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")); List urls = new ArrayList<>(); for (File path : paths) { + if (path.getName().startsWith("jenkins-test-harness")) { + throw new IllegalStateException("Refusing to load the Jenkins test harness in production (via " + + atts.getValue("Short-Name") + ")"); + } urls.add(path.toURI().toURL()); } 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; } @@ -570,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/DescriptorExtensionList.java b/core/src/main/java/hudson/DescriptorExtensionList.java index c92b6ae206c0..c658c83fe1fc 100644 --- a/core/src/main/java/hudson/DescriptorExtensionList.java +++ b/core/src/main/java/hudson/DescriptorExtensionList.java @@ -145,7 +145,7 @@ public T newInstanceFromRadioList(JSONObject config) throws FormException { if (config.isNullObject()) return null; // none was selected int idx = config.getInt("value"); - return get(idx).newInstance(Stapler.getCurrentRequest(), config); + return get(idx).newInstance(Stapler.getCurrentRequest2(), config); } /** 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/ExpressionFactory2.java b/core/src/main/java/hudson/ExpressionFactory2.java index 7fcec22e7604..1bc99160439b 100644 --- a/core/src/main/java/hudson/ExpressionFactory2.java +++ b/core/src/main/java/hudson/ExpressionFactory2.java @@ -12,7 +12,7 @@ import org.apache.commons.jelly.expression.ExpressionSupport; import org.apache.commons.jexl.JexlContext; import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.springframework.security.access.AccessDeniedException; /** @@ -78,7 +78,7 @@ public Object evaluate(JellyContext context) { // let the security exception pass through throw e; } catch (Exception e) { - StaplerRequest currentRequest = Stapler.getCurrentRequest(); + StaplerRequest2 currentRequest = Stapler.getCurrentRequest2(); LOGGER.log(Level.WARNING, "Caught exception evaluating: " + expression + " in " + (currentRequest != null ? currentRequest.getOriginalRequestURI() : "?") + ". Reason: " + e, e); return null; } finally { 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/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java index 2b6866ef5a3c..c333b2de9ff2 100644 --- a/core/src/main/java/hudson/ExtensionList.java +++ b/core/src/main/java/hudson/ExtensionList.java @@ -459,6 +459,31 @@ public static ExtensionList create(Jenkins jenkins, Class type) { return all.get(0); } + /** + * Convenience method allowing lookup of the instance of a given type with the highest ordinal. + * Equivalent to {@code ExtensionList.lookup(type).get(0)} if there is at least one instance, + * and throws an {@link IllegalStateException} otherwise if no instance could be found. + * + * @param type The type to look up. + * @return the singleton instance of the given type in its list. + * @throws IllegalStateException if there are no instances + * + * @since 2.435 + */ + public static @NonNull U lookupFirst(Class type) { + var all = lookup(type); + if (!all.isEmpty()) { + return all.get(0); + } else { + if (Main.isUnitTest) { + throw new IllegalStateException("Found no instances of " + type.getName() + + " registered (possible annotation processor issue); try using `mvn clean test -Dtest=…` rather than an IDE test runner"); + } else { + throw new IllegalStateException("Found no instances of " + type.getName() + " registered"); + } + } + } + /** * Places to store static-scope legacy instances. */ diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index cdc057ef9c54..e48d7b45108f 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -28,10 +28,9 @@ import static hudson.Util.fileToPath; import static hudson.Util.fixEmpty; +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; @@ -60,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; @@ -79,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; @@ -86,6 +85,7 @@ import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.CopyOption; import java.nio.file.FileSystemException; import java.nio.file.FileSystems; @@ -121,22 +121,23 @@ 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.agents.ControllerToAgentFileCallable; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; import jenkins.util.ContextResettingExecutorService; import jenkins.util.SystemProperties; import jenkins.util.VirtualFile; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload2.core.FileItem; import org.apache.commons.io.input.CountingInputStream; -import org.apache.commons.lang.StringUtils; 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; @@ -320,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(); @@ -344,28 +345,28 @@ public static String normalize(@NonNull String path) { String token = tokens.get(i); if (token.equals(".")) { tokens.remove(i); - if (tokens.size() > 0) + if (!tokens.isEmpty()) tokens.remove(i > 0 ? i - 1 : i); } else if (token.equals("..")) { if (i == 0) { // If absolute path, just remove: /../something // If relative path, not collapsible so leave as-is tokens.remove(0); - if (tokens.size() > 0) token += tokens.remove(0); + if (!tokens.isEmpty()) token += tokens.remove(0); if (!isAbsolute) buf.append(token); } else { // Normalize: remove something/.. plus separator before/after i -= 2; for (int j = 0; j < 3; j++) tokens.remove(i); if (i > 0) tokens.remove(i - 1); - else if (tokens.size() > 0) tokens.remove(0); + else if (!tokens.isEmpty()) tokens.remove(0); } } else i += 2; } // Recombine tokens for (String token : tokens) buf.append(token); - if (buf.length() == 0) buf.append('.'); + if (buf.isEmpty()) buf.append('.'); return buf.toString(); } @@ -482,7 +483,6 @@ public int zip(OutputStream out, DirScanner scanner) throws IOException, Interru * @return The number of files/directories archived. * This is only really useful to check for a situation where nothing */ - @Restricted(NoExternalUse.class) public int zip(OutputStream out, DirScanner scanner, String verificationRoot, String prefix, OpenOption... openOptions) throws IOException, InterruptedException { ArchiverFactory archiverFactory = prefix == null ? ArchiverFactory.ZIP : ArchiverFactory.createZipWithPrefix(prefix, openOptions); return archive(archiverFactory, out, scanner, verificationRoot, openOptions); @@ -514,28 +514,13 @@ public int archive(final ArchiverFactory factory, OutputStream os, final DirScan * @return The number of files/directories archived. * This is only really useful to check for a situation where nothing */ - @Restricted(NoExternalUse.class) public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner, String verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException { final OutputStream out = channel != null ? new RemoteOutputStream(os) : os; return act(new Archive(factory, out, scanner, verificationRoot, openOptions)); } - private static class Archive extends MasterToSlaveFileCallable { - private final ArchiverFactory factory; - private final OutputStream out; - private final DirScanner scanner; - private final String verificationRoot; - private OpenOption[] openOptions; - - Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, OpenOption... openOptions) { - this.factory = factory; - this.out = out; - this.scanner = scanner; - this.verificationRoot = verificationRoot; - this.openOptions = openOptions; - } - + private record Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner, String verificationRoot, OpenOption... openOptions) implements ControllerToAgentFileCallable { @Override public Integer invoke(File f, VirtualChannel channel) throws IOException { try (Archiver a = factory.create(out)) { @@ -543,8 +528,6 @@ public Integer invoke(File f, VirtualChannel channel) throws IOException { return a.countEntries(); } } - - private static final long serialVersionUID = 1L; } public int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException { @@ -761,7 +744,6 @@ public String invoke(File f, VirtualChannel channel) throws IOException { } } - @Restricted(NoExternalUse.class) public boolean hasSymlink(FilePath verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException { return act(new HasSymlink(verificationRoot == null ? null : verificationRoot.remote, openOptions)); } @@ -782,7 +764,6 @@ public Boolean invoke(File f, VirtualChannel channel) throws IOException { } } - @Restricted(NoExternalUse.class) public boolean containsSymlink(FilePath verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException { return !list(new SymlinkRetainingFileFilter(verificationRoot, openOptions)).isEmpty(); } @@ -890,15 +871,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 @@ -962,7 +936,7 @@ public Void invoke(File dir, VirtualChannel channel) throws IOException { * * * @param archive - * The resource that represents the tgz/zip file. This URL must support the {@code Last-Modified} header. + * The resource that represents the tgz/zip file. This URL must support the {@code Last-Modified} header or the {@code ETag} header. * (For example, you could use {@link ClassLoader#getResource}.) * @param listener * If non-null, a message will be printed to this listener once this method decides to @@ -984,12 +958,18 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen try { FilePath timestamp = this.child(".timestamp"); long lastModified = timestamp.lastModified(); + // https://httpwg.org/specs/rfc9110.html#field.etag is the ETag specification + // Read previously stored ETag if timestamp is available + String etag = timestamp.exists() ? fixEmptyAndTrim(timestamp.readToString()) : null; URLConnection con; try { con = ProxyConfiguration.open(archive); if (lastModified != 0) { con.setIfModifiedSince(lastModified); } + if (etag != null) { + con.setRequestProperty("If-None-Match", etag); + } con.connect(); } catch (IOException x) { if (this.exists()) { @@ -1001,8 +981,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) { @@ -1016,7 +995,7 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen return false; } } - if (lastModified != 0) { + if (lastModified != 0 || etag != null) { if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { return false; } else if (responseCode != HttpURLConnection.HTTP_OK) { @@ -1027,8 +1006,12 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } long sourceTimestamp = con.getLastModified(); + String resultEtag = fixEmptyAndTrim(con.getHeaderField("ETag")); if (this.exists()) { + if (equalETags(etag, resultEtag)) { + return false; // already up to date + } if (lastModified != 0 && sourceTimestamp == lastModified) return false; // already up to date this.deleteContents(); @@ -1042,6 +1025,10 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen // First try to download from the agent machine. try { act(new Unpack(archive)); + if (resultEtag != null && !equalETags(etag, resultEtag)) { + /* Store the ETag value in the timestamp file for later use */ + timestamp.write(resultEtag, "UTF-8"); + } timestamp.touch(sourceTimestamp); return true; } catch (IOException x) { @@ -1061,6 +1048,10 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen throw new IOException(String.format("Failed to unpack %s (%d bytes read of total %d)", archive, cis.getByteCount(), con.getContentLength()), e); } + if (resultEtag != null && !equalETags(etag, resultEtag)) { + /* Store the ETag value in the timestamp file for later use */ + timestamp.write(resultEtag, "UTF-8"); + } timestamp.touch(sourceTimestamp); return true; } catch (IOException e) { @@ -1068,6 +1059,25 @@ private boolean installIfNecessaryFrom(@NonNull URL archive, @NonNull TaskListen } } + /* Return true if etag1 equals etag2 as defined by the etag specification + https://httpwg.org/specs/rfc9110.html#field.etag + */ + private boolean equalETags(String etag1, String etag2) { + if (etag1 == null || etag2 == null) { + return false; + } + if (etag1.equals(etag2)) { + return true; + } + /* Weak tags are identified by leading characters "W/" as a marker */ + /* Weak tag marker must not be considered in tag comparison. + This implements the weak comparison in the specification at + https://httpwg.org/specs/rfc9110.html#field.etag */ + String opaqueTag1 = etag1.startsWith("W/") ? etag1.substring(2) : etag1; + String opaqueTag2 = etag2.startsWith("W/") ? etag2.substring(2) : etag2; + return opaqueTag1.equals(opaqueTag2); + } + // this reads from arbitrary URL private static final class Unpack extends MasterToSlaveFileCallable { private final URL archive; @@ -1132,7 +1142,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) { @@ -1146,15 +1158,18 @@ 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}. - * Warning: implementations must be serializable, so prefer a static nested class to an inner class. - * - *

- * Subtypes would likely want to extend from either {@link MasterToSlaveCallable} - * or {@link SlaveToMasterFileCallable}. - * + * A typical implementation would be a {@code record} implementing {@link ControllerToAgentFileCallable}. * @see FilePath#act(FileCallable) */ public interface FileCallable extends Serializable, RoleSensitive { @@ -1408,7 +1423,7 @@ private static class DeleteSuffixesRecursive extends MasterToSlaveFileCallable path.toFile()); + Util.deleteRecursive(file.toPath(), Path::toFile); } } @@ -1439,7 +1454,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; } } @@ -1456,7 +1471,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; } } @@ -1678,7 +1693,7 @@ public String invoke(File dir, VirtualChannel channel) throws IOException { public FilePath createTempDir(final String prefix, final String suffix) throws IOException, InterruptedException { try { String[] s; - if (StringUtils.isBlank(suffix)) { + if (suffix == null || suffix.isBlank()) { s = new String[]{prefix, "tmp"}; // see File.createTempFile - tmp is used if suffix is null } else { s = new String[]{prefix, suffix}; @@ -2019,7 +2034,6 @@ public List list() throws IOException, InterruptedException { * @param openOptions the options to apply when opening. * @return Direct children of this directory. */ - @Restricted(NoExternalUse.class) @NonNull public List list(FilePath verificationRoot, OpenOption... openOptions) throws IOException, InterruptedException { return list(new OptionalDiscardingFileFilter(verificationRoot, openOptions)); @@ -2184,7 +2198,6 @@ public InputStream read() throws IOException, InterruptedException { return read(null, new OpenOption[0]); } - @Restricted(NoExternalUse.class) public InputStream read(FilePath rootPath, OpenOption... openOptions) throws IOException, InterruptedException { String rootPathString = rootPath == null ? null : rootPath.remote; if (channel == null) { @@ -2199,7 +2212,6 @@ public InputStream read(FilePath rootPath, OpenOption... openOptions) throws IOE return p.getIn(); } - @Restricted(NoExternalUse.class) public static InputStream newInputStreamDenyingSymlinkAsNeeded(File file, String verificationRoot, OpenOption... openOptions) throws IOException { InputStream inputStream = null; try { @@ -2216,7 +2228,6 @@ public static InputStream newInputStreamDenyingSymlinkAsNeeded(File file, String return inputStream; } - @Restricted(NoExternalUse.class) public static InputStream openInputStream(File file, OpenOption[] openOptions) throws IOException { return Files.newInputStream(fileToPath(file), stripLocalOptions(openOptions)); } @@ -2252,7 +2263,6 @@ private static void denyTmpDir(File file, String root, OpenOption... openOptions } } - @Restricted(NoExternalUse.class) public static boolean isSymlink(File file, String root, OpenOption... openOptions) { if (isNoFollowLink(openOptions)) { if (Util.isSymlink(file.toPath())) { @@ -2268,7 +2278,6 @@ private static boolean isSymlink(VisitorInfo visitorInfo) { return isSymlink(visitorInfo.f, visitorInfo.verificationRoot, visitorInfo.openOptions); } - @Restricted(NoExternalUse.class) public static boolean isTmpDir(File file, String root, OpenOption... openOptions) { if (isIgnoreTmpDirs(openOptions)) { if (isTmpDir(file)) { @@ -2280,7 +2289,6 @@ public static boolean isTmpDir(File file, String root, OpenOption... openOptions return false; } - @Restricted(NoExternalUse.class) public static boolean isTmpDir(String filename, OpenOption... openOptions) { if (isIgnoreTmpDirs(openOptions)) { return isTmpDir(filename); @@ -2300,12 +2308,10 @@ private static boolean isTmpDir(String filename) { return filename.length() > WorkspaceList.TMP_DIR_SUFFIX.length() && filename.endsWith(WorkspaceList.TMP_DIR_SUFFIX); } - @Restricted(NoExternalUse.class) public static boolean isNoFollowLink(OpenOption... openOptions) { return Arrays.asList(openOptions).contains(LinkOption.NOFOLLOW_LINKS); } - @Restricted(NoExternalUse.class) public static boolean isIgnoreTmpDirs(OpenOption... openOptions) { return Arrays.asList(openOptions).contains(DisplayOption.IGNORE_TMP_DIRS); } @@ -2435,7 +2441,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]; @@ -2810,8 +2816,8 @@ public int copyRecursiveTo(final DirScanner scanner, final FilePath target, fina // local -> remote copy final Pipe pipe = Pipe.createLocalToRemote(); - Future future = target.actAsync(new ReadFromTar(target, pipe, description, compression)); - Future future2 = actAsync(new WriteToTar(scanner, pipe, compression)); + Future future = target.actAsync(new ReadFromTar(target, pipe, description, compression, StandardCharsets.UTF_8)); + Future future2 = actAsync(new WriteToTar(scanner, pipe, compression, StandardCharsets.UTF_8)); try { // JENKINS-9540 in case the reading side failed, report that error first future.get(); @@ -2823,9 +2829,9 @@ public int copyRecursiveTo(final DirScanner scanner, final FilePath target, fina // remote -> local copy final Pipe pipe = Pipe.createRemoteToLocal(); - Future future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner, compression)); + Future future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner, compression, StandardCharsets.UTF_8)); try { - readFromTar(remote + '/' + description, new File(target.remote), compression.extract(pipe.getIn())); + readFromTar(remote + '/' + description, new File(target.remote), compression.extract(pipe.getIn()), StandardCharsets.UTF_8); } catch (IOException e) { // BuildException or IOException try { future.get(3, TimeUnit.SECONDS); @@ -2938,12 +2944,14 @@ private static class ReadFromTar extends MasterToSlaveFileCallable { private final String description; private final TarCompression compression; private final FilePath target; + private final String filenamesEncoding; - ReadFromTar(FilePath target, Pipe pipe, String description, @NonNull TarCompression compression) { + ReadFromTar(FilePath target, Pipe pipe, String description, @NonNull TarCompression compression, Charset filenamesEncoding) { this.target = target; this.pipe = pipe; this.description = description; this.compression = compression; + this.filenamesEncoding = filenamesEncoding.name(); } private static final long serialVersionUID = 1L; @@ -2951,7 +2959,7 @@ private static class ReadFromTar extends MasterToSlaveFileCallable { @Override public Void invoke(File f, VirtualChannel channel) throws IOException { try (InputStream in = pipe.getIn()) { - readFromTar(target.remote + '/' + description, f, compression.extract(in)); + readFromTar(target.remote + '/' + description, f, compression.extract(in), Charset.forName(filenamesEncoding)); return null; } } @@ -2961,18 +2969,20 @@ private static class WriteToTar extends MasterToSlaveFileCallable { private final DirScanner scanner; private final Pipe pipe; private final TarCompression compression; + private final String filenamesEncoding; - WriteToTar(DirScanner scanner, Pipe pipe, @NonNull TarCompression compression) { + WriteToTar(DirScanner scanner, Pipe pipe, @NonNull TarCompression compression, Charset filenamesEncoding) { this.scanner = scanner; this.pipe = pipe; this.compression = compression; + this.filenamesEncoding = filenamesEncoding.name(); } private static final long serialVersionUID = 1L; @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return writeToTar(f, scanner, compression.compress(pipe.getOut())); + return writeToTar(f, scanner, compression.compress(pipe.getOut()), Charset.forName(filenamesEncoding)); } } @@ -2981,17 +2991,19 @@ private static class CopyRecursiveRemoteToLocal extends MasterToSlaveFileCallabl private final Pipe pipe; private final DirScanner scanner; private final TarCompression compression; + private final String filenamesEncoding; - CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner, @NonNull TarCompression compression) { + CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner, @NonNull TarCompression compression, Charset filenamesEncoding) { this.pipe = pipe; this.scanner = scanner; this.compression = compression; + this.filenamesEncoding = filenamesEncoding.name(); } @Override public Integer invoke(File f, VirtualChannel channel) throws IOException { try (OutputStream out = pipe.getOut()) { - return writeToTar(f, scanner, compression.compress(out)); + return writeToTar(f, scanner, compression.compress(out), Charset.forName(filenamesEncoding)); } } } @@ -3023,24 +3035,27 @@ public int tar(OutputStream out, DirScanner scanner) throws IOException, Interru * @return * number of files/directories that are written. */ - private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException { - Archiver tw = ArchiverFactory.TAR.create(out); + private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out, Charset filenamesEncoding) throws IOException { + Archiver tw = ArchiverFactory.TAR.create(out, filenamesEncoding); try (tw) { scanner.scan(baseDir, tw); } return tw.countEntries(); } + private static void readFromTar(String name, File baseDir, InputStream in) throws IOException { + readFromTar(name, baseDir, in, Charset.defaultCharset()); + } + /** * 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) throws IOException { + 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)) { - 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( @@ -3474,7 +3489,7 @@ public FormValidation validateRelativePath(String value, boolean errorIfNotExist } private static void checkPermissionForValidate() { - AccessControlled subject = Stapler.getCurrentRequest().findAncestorObject(AbstractProject.class); + AccessControlled subject = Stapler.getCurrentRequest2().findAncestorObject(AbstractProject.class); if (subject == null) Jenkins.get().checkPermission(Jenkins.MANAGE); else @@ -3675,7 +3690,6 @@ public ExplicitlySpecifiedDirScanner(Map files) { /** * Wraps {@link FileVisitor} to ignore symlinks. */ - @Restricted(NoExternalUse.class) public static FileVisitor ignoringSymlinks(final FileVisitor v, String verificationRoot, OpenOption... openOptions) { return validatingVisitor(FilePath::isNoFollowLink, visitorInfo -> !isSymlink(visitorInfo), @@ -3685,7 +3699,6 @@ public static FileVisitor ignoringSymlinks(final FileVisitor v, String verificat /** * Wraps {@link FileVisitor} to ignore tmp directories. */ - @Restricted(NoExternalUse.class) public static FileVisitor ignoringTmpDirs(final FileVisitor v, String verificationRoot, OpenOption... openOptions) { return validatingVisitor(FilePath::isIgnoreTmpDirs, visitorInfo -> !isTmpDir(visitorInfo), @@ -3728,10 +3741,7 @@ private static File mkdirsE(File dir) throws IOException { /** * Check if the relative child is really a descendant after symlink resolution if any. - * - * TODO un-restrict it in a weekly after the patch */ - @Restricted(NoExternalUse.class) public boolean isDescendant(@NonNull String potentialChildRelativePath) throws IOException, InterruptedException { return act(new IsDescendant(potentialChildRelativePath)); } diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 7f3411406780..150b1b658c16 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -94,6 +94,14 @@ import hudson.views.MyViewsTabBar; import hudson.views.ViewsTabBar; import hudson.widgets.RenderOnDemandClosure; +import io.jenkins.servlet.ServletExceptionWrapper; +import io.jenkins.servlet.http.CookieWrapper; +import io.jenkins.servlet.http.HttpServletRequestWrapper; +import io.jenkins.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.PrintStream; @@ -116,6 +124,9 @@ import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.DecimalFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -144,10 +155,8 @@ import java.util.logging.SimpleFormatter; import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jenkins.console.ConsoleUrlProvider; +import jenkins.console.WithConsoleUrl; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; @@ -161,7 +170,6 @@ import org.apache.commons.jelly.XMLOutput; import org.apache.commons.jexl.parser.ASTSizeFunction; import org.apache.commons.jexl.util.Introspector; -import org.apache.commons.lang.StringUtils; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; import org.jvnet.tiger_types.Types; @@ -172,7 +180,9 @@ import org.kohsuke.stapler.RawHtmlArgument; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerResponse2; import org.springframework.security.access.AccessDeniedException; /** @@ -217,12 +227,12 @@ public static boolean isMatrixProject(Object o) { } public static String xsDate(Calendar cal) { - return Util.XS_DATETIME_FORMATTER.format(cal.getTime()); + return Util.XS_DATETIME_FORMATTER2.format(cal.toInstant()); } @Restricted(NoExternalUse.class) public static String iso8601DateTime(Date date) { - return Util.XS_DATETIME_FORMATTER.format(date); + return Util.XS_DATETIME_FORMATTER2.format(date.toInstant()); } /** @@ -234,7 +244,7 @@ public static String localDate(Date date) { } public static String rfc822Date(Calendar cal) { - return Util.RFC822_DATETIME_FORMATTER.format(cal.getTime()); + return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(cal.toInstant(), ZoneId.systemDefault())); } /** @@ -272,8 +282,8 @@ public static boolean isExtensionsAvailable() { } public static void initPageVariables(JellyContext context) { - StaplerRequest currentRequest = Stapler.getCurrentRequest(); - currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse()); + StaplerRequest2 currentRequest = Stapler.getCurrentRequest2(); + currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse2()); String rootURL = currentRequest.getContextPath(); Functions h = new Functions(); @@ -312,8 +322,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); @@ -369,7 +378,10 @@ public static String addSuffix(int n, String singular, String plural) { return buf.toString(); } - public static RunUrl decompose(StaplerRequest req) { + /** + * @since 2.475 + */ + public static RunUrl decompose(StaplerRequest2 req) { List ancestors = req.getAncestors(); // find the first and last Run instances @@ -402,12 +414,20 @@ public static RunUrl decompose(StaplerRequest req) { return new RunUrl((Run) f.getObject(), head, base, rest); } + /** + * @deprecated use {@link #decompose(StaplerRequest2)} + */ + @Deprecated + public static RunUrl decompose(StaplerRequest req) { + return decompose(StaplerRequest.toStaplerRequest2(req)); + } + /** * If we know the user's screen resolution, return it. Otherwise null. * @since 1.213 */ public static Area getScreenResolution() { - Cookie res = Functions.getCookie(Stapler.getCurrentRequest(), "screenResolution"); + Cookie res = Functions.getCookie(Stapler.getCurrentRequest2(), "screenResolution"); if (res != null) return Area.parse(res.getValue()); return null; @@ -589,6 +609,9 @@ public static Iterable reverse(Collection collection) { return list; } + /** + * @since 2.475 + */ public static Cookie getCookie(HttpServletRequest req, String name) { Cookie[] cookies = req.getCookies(); if (cookies != null) { @@ -601,12 +624,31 @@ public static Cookie getCookie(HttpServletRequest req, String name) { return null; } + /** + * @deprecated use {@link #getCookie(HttpServletRequest, String)} + */ + @Deprecated + public static javax.servlet.http.Cookie getCookie(javax.servlet.http.HttpServletRequest req, String name) { + return CookieWrapper.fromJakartaServletHttpCookie(getCookie(HttpServletRequestWrapper.toJakartaHttpServletRequest(req), name)); + } + + /** + * @since 2.475 + */ public static String getCookie(HttpServletRequest req, String name, String defaultValue) { Cookie c = getCookie(req, name); if (c == null || c.getValue() == null) return defaultValue; return c.getValue(); } + /** + * @deprecated use {@link #getCookie(HttpServletRequest, String, String)} + */ + @Deprecated + public static String getCookie(javax.servlet.http.HttpServletRequest req, String name, String defaultValue) { + return getCookie(HttpServletRequestWrapper.toJakartaHttpServletRequest(req), name, defaultValue); + } + private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+"); @Restricted(NoExternalUse.class) @@ -690,13 +732,13 @@ public static String getUserTimeZone() { } @Restricted(NoExternalUse.class) - public static String getUserTimeZonePostfix() { + public static String getUserTimeZonePostfix(Date date) { if (!isUserTimeZoneOverride()) { return ""; } TimeZone tz = TimeZone.getTimeZone(getUserTimeZone()); - return tz.getDisplayName(tz.observesDaylightTime(), TimeZone.SHORT); + return tz.getDisplayName(tz.inDaylightTime(date), TimeZone.SHORT, getCurrentLocale()); } @Restricted(NoExternalUse.class) @@ -710,8 +752,10 @@ public static long getHourLocalTimezone() { * Finds the given object in the ancestor list and returns its URL. * This is used to determine the "current" URL assigned to the given object, * so that one can compute relative URLs from it. + * + * @since 2.475 */ - public static String getNearestAncestorUrl(StaplerRequest req, Object it) { + public static String getNearestAncestorUrl(StaplerRequest2 req, Object it) { List list = req.getAncestors(); for (int i = list.size() - 1; i >= 0; i--) { Ancestor anc = (Ancestor) list.get(i); @@ -721,11 +765,19 @@ public static String getNearestAncestorUrl(StaplerRequest req, Object it) { return null; } + /** + * @deprecated use {@link #getNearestAncestorUrl(StaplerRequest2, Object)} + */ + @Deprecated + public static String getNearestAncestorUrl(StaplerRequest req, Object it) { + return getNearestAncestorUrl(StaplerRequest.toStaplerRequest2(req), it); + } + /** * Finds the inner-most {@link SearchableModelObject} in scope. */ public static String getSearchURL() { - List list = Stapler.getCurrentRequest().getAncestors(); + List list = Stapler.getCurrentRequest2().getAncestors(); for (int i = list.size() - 1; i >= 0; i--) { Ancestor anc = (Ancestor) list.get(i); if (anc.getObject() instanceof SearchableModelObject) @@ -885,7 +937,7 @@ public static void checkPermission(Object object, Permission permission) throws if (object instanceof AccessControlled) checkPermission((AccessControlled) object, permission); else { - List ancs = Stapler.getCurrentRequest().getAncestors(); + List ancs = Stapler.getCurrentRequest2().getAncestors(); for (Ancestor anc : Iterators.reverse(ancs)) { Object o = anc.getObject(); if (o instanceof AccessControlled) { @@ -917,7 +969,7 @@ public static boolean hasPermission(Object object, Permission permission) throws if (object instanceof AccessControlled) return ((AccessControlled) object).hasPermission(permission); else { - List ancs = Stapler.getCurrentRequest().getAncestors(); + List ancs = Stapler.getCurrentRequest2().getAncestors(); for (Ancestor anc : Iterators.reverse(ancs)) { Object o = anc.getObject(); if (o instanceof AccessControlled) { @@ -928,10 +980,13 @@ public static boolean hasPermission(Object object, Permission permission) throws } } - public static void adminCheck(StaplerRequest req, StaplerResponse rsp, Object required, Permission permission) throws IOException, ServletException { + /** + * @since 2.475 + */ + public static void adminCheck(StaplerRequest2 req, StaplerResponse2 rsp, Object required, Permission permission) throws IOException, ServletException { // this is legacy --- all views should be eventually converted to // the permission based model. - if (required != null && !Hudson.adminCheck(req, rsp)) { + if (required != null && !Hudson.adminCheck(StaplerRequest.fromStaplerRequest2(req), StaplerResponse.fromStaplerResponse2(rsp))) { // check failed. commit the FORBIDDEN response, then abort. rsp.setStatus(HttpServletResponse.SC_FORBIDDEN); rsp.getOutputStream().close(); @@ -943,10 +998,24 @@ public static void adminCheck(StaplerRequest req, StaplerResponse rsp, Object re checkPermission(permission); } + /** + * @deprecated use {@link #adminCheck(StaplerRequest2, StaplerResponse2, Object, Permission)} + */ + @Deprecated + public static void adminCheck(StaplerRequest req, StaplerResponse rsp, Object required, Permission permission) throws IOException, javax.servlet.ServletException { + try { + adminCheck(StaplerRequest.toStaplerRequest2(req), StaplerResponse.toStaplerResponse2(rsp), required, permission); + } catch (ServletException e) { + throw ServletExceptionWrapper.fromJakartaServletException(e); + } + } + /** * Infers the hudson installation URL from the given request. + * + * @since 2.475 */ - public static String inferHudsonURL(StaplerRequest req) { + public static String inferHudsonURL(StaplerRequest2 req) { String rootUrl = Jenkins.get().getRootUrl(); if (rootUrl != null) // prefer the one explicitly configured, to work with load-balancer, frontend, etc. @@ -960,13 +1029,21 @@ public static String inferHudsonURL(StaplerRequest req) { return buf.toString(); } + /** + * @deprecated use {@link #inferHudsonURL(StaplerRequest2)} + */ + @Deprecated + public static String inferHudsonURL(StaplerRequest req) { + return inferHudsonURL(StaplerRequest.toStaplerRequest2(req)); + } + /** * Returns the link to be displayed in the footer of the UI. */ public static String getFooterURL() { if (footerURL == null) { footerURL = SystemProperties.getString("hudson.footerURL"); - if (StringUtils.isBlank(footerURL)) { + if (footerURL == null || footerURL.isBlank()) { footerURL = "https://www.jenkins.io/"; } } @@ -1223,7 +1300,7 @@ public static boolean hasAnyPermission(Object object, Permission[] permissions) if (object instanceof AccessControlled) return hasAnyPermission((AccessControlled) object, permissions); else { - AccessControlled ac = Stapler.getCurrentRequest().findAncestorObject(AccessControlled.class); + AccessControlled ac = Stapler.getCurrentRequest2().findAncestorObject(AccessControlled.class); if (ac != null) { return hasAnyPermission(ac, permissions); } @@ -1261,7 +1338,7 @@ public static void checkAnyPermission(Object object, Permission[] permissions) t if (object instanceof AccessControlled) checkAnyPermission((AccessControlled) object, permissions); else { - List ancs = Stapler.getCurrentRequest().getAncestors(); + List ancs = Stapler.getCurrentRequest2().getAncestors(); for (Ancestor anc : Iterators.reverse(ancs)) { Object o = anc.getObject(); if (o instanceof AccessControlled) { @@ -1332,7 +1409,7 @@ public static String getRelativeLinkTo(Item p) { Map ancestors = new HashMap<>(); View view = null; - StaplerRequest request = Stapler.getCurrentRequest(); + StaplerRequest2 request = Stapler.getCurrentRequest2(); for (Ancestor a : request.getAncestors()) { ancestors.put(a.getObject(), a.getRelativePath()); if (a.getObject() instanceof View) @@ -1418,7 +1495,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(); @@ -1678,7 +1755,7 @@ public static String getViewResource(Object it, String path) { if (it instanceof Descriptor) clazz = ((Descriptor) it).clazz; - String buf = Stapler.getCurrentRequest().getContextPath() + Jenkins.VIEW_RESOURCE_PATH + '/' + + String buf = Stapler.getCurrentRequest2().getContextPath() + Jenkins.VIEW_RESOURCE_PATH + '/' + clazz.getName().replace('.', '/').replace('$', '/') + '/' + path; return buf; @@ -1686,7 +1763,7 @@ public static String getViewResource(Object it, String path) { public static boolean hasView(Object it, String path) throws IOException { if (it == null) return false; - return Stapler.getCurrentRequest().getView(it, path) != null; + return Stapler.getCurrentRequest2().getView(it, path) != null; } /** @@ -1869,7 +1946,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); @@ -1897,10 +1974,21 @@ public static String joinPath(String... components) { return null; } if (urlName.startsWith("/")) - return joinPath(Stapler.getCurrentRequest().getContextPath(), urlName); + return joinPath(Stapler.getCurrentRequest2().getContextPath(), urlName); else // relative URL name - return joinPath(Stapler.getCurrentRequest().getContextPath() + '/' + itUrl, urlName); + return joinPath(Stapler.getCurrentRequest2().getContextPath() + '/' + itUrl, urlName); + } + + /** + * Computes the link to the console for the run for the specified object, taking {@link ConsoleUrlProvider} into account. + * @param withConsoleUrl the object to compute a console url for (can be {@link Run}, a {@code PlaceholderExecutable}...) + * @return the absolute URL for accessing the build console for the given object, or null if there is no console URL defined for the object. + * @since 2.433 + */ + public static @CheckForNull String getConsoleUrl(WithConsoleUrl withConsoleUrl) { + String consoleUrl = withConsoleUrl.getConsoleUrl(); + return consoleUrl != null ? Stapler.getCurrentRequest().getContextPath() + '/' + consoleUrl : null; } /** @@ -1946,7 +2034,7 @@ public String getServerName() { } catch (MalformedURLException e) { // fall back to HTTP request } - return Stapler.getCurrentRequest().getServerName(); + return Stapler.getCurrentRequest2().getServerName(); } /** @@ -1958,8 +2046,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; @@ -1972,8 +2059,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()); @@ -1986,7 +2072,7 @@ public void calcCheckUrl(Map attributes, String userDefined, Object descriptor, * Used in {@code task.jelly} to decide if the page should be highlighted. */ public boolean hyperlinkMatchesCurrentPage(String href) { - String url = Stapler.getCurrentRequest().getRequestURL().toString(); + String url = Stapler.getCurrentRequest2().getRequestURL().toString(); if (href == null || href.length() <= 1) return ".".equals(href) && url.endsWith("/"); url = URLDecoder.decode(url, StandardCharsets.UTF_8); href = URLDecoder.decode(href, StandardCharsets.UTF_8); @@ -2036,7 +2122,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; } @@ -2045,12 +2131,23 @@ public static List> getCrumbIssuerDescriptors() { return CrumbIssuer.all(); } - public static String getCrumb(StaplerRequest req) { + /** + * @since 2.475 + */ + public static String getCrumb(StaplerRequest2 req) { Jenkins h = Jenkins.getInstanceOrNull(); CrumbIssuer issuer = h != null ? h.getCrumbIssuer() : null; return issuer != null ? issuer.getCrumb(req) : ""; } + /** + * @deprecated use {@link #getCrumb(StaplerRequest2)} + */ + @Deprecated + public static String getCrumb(StaplerRequest req) { + return getCrumb(req != null ? StaplerRequest.toStaplerRequest2(req) : null); + } + public static String getCrumbRequestField() { Jenkins h = Jenkins.getInstanceOrNull(); CrumbIssuer issuer = h != null ? h.getCrumbIssuer() : null; @@ -2063,7 +2160,7 @@ public static Date getCurrentTime() { public static Locale getCurrentLocale() { Locale locale = null; - StaplerRequest req = Stapler.getCurrentRequest(); + StaplerRequest2 req = Stapler.getCurrentRequest2(); if (req != null) locale = req.getLocale(); if (locale == null) @@ -2076,7 +2173,7 @@ public static Locale getCurrentLocale() { * from {@link ConsoleAnnotatorFactory}s and {@link ConsoleAnnotationDescriptor}s. */ public static String generateConsoleAnnotationScriptAndStylesheet() { - String cp = Stapler.getCurrentRequest().getContextPath() + Jenkins.RESOURCE_PATH; + String cp = Stapler.getCurrentRequest2().getContextPath() + Jenkins.RESOURCE_PATH; StringBuilder buf = new StringBuilder(); for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) { String path = cp + "/extensionList/" + ConsoleAnnotatorFactory.class.getName() + "/" + f.getClass().getName(); @@ -2129,7 +2226,7 @@ public String getPasswordValue(Object o) { } /* Mask from Extended Read */ - StaplerRequest req = Stapler.getCurrentRequest(); + StaplerRequest2 req = Stapler.getCurrentRequest2(); if (o instanceof Secret || Secret.BLANK_NONSECRET_PASSWORD_FIELDS_WITHOUT_ITEM_CONFIGURE) { if (req != null) { Item item = req.findAncestorObject(Item.class); @@ -2175,7 +2272,7 @@ private String getJellyViewsInformationForCurrentRequest() { int firstPeriod = part.indexOf("."); return slash > 0 && firstPeriod > 0 && slash < firstPeriod; }).collect(Collectors.joining(" ")); - if (StringUtils.isBlank(views)) { + if (views == null || views.isBlank()) { // fallback to full thread name if there are no apparent views return threadName; } @@ -2223,8 +2320,19 @@ public static boolean isWipeOutPermissionEnabled() { return SystemProperties.getBoolean("hudson.security.WipeOutPermission"); } + @Deprecated public static String createRenderOnDemandProxy(JellyContext context, String attributesToCapture) { - return Stapler.getCurrentRequest().createJavaScriptProxy(new RenderOnDemandClosure(context, attributesToCapture)); + return Stapler.getCurrentRequest2().createJavaScriptProxy(new RenderOnDemandClosure(context, attributesToCapture)); + } + + /** + * Called from renderOnDemand.jelly to generate the parameters for the proxy object generation. + * + * @since 2.475 + */ + @Restricted(NoExternalUse.class) + public static StaplerRequest2.RenderOnDemandParameters createRenderOnDemandProxyParameters(JellyContext context, String attributesToCapture) { + return Stapler.getCurrentRequest2().createJavaScriptProxyParameters(new RenderOnDemandClosure(context, attributesToCapture)); } public static String getCurrentDescriptorByNameUrl() { @@ -2233,18 +2341,18 @@ public static String getCurrentDescriptorByNameUrl() { public static String setCurrentDescriptorByNameUrl(String value) { String o = getCurrentDescriptorByNameUrl(); - Stapler.getCurrentRequest().setAttribute("currentDescriptorByNameUrl", value); + Stapler.getCurrentRequest2().setAttribute("currentDescriptorByNameUrl", value); return o; } public static void restoreCurrentDescriptorByNameUrl(String old) { - Stapler.getCurrentRequest().setAttribute("currentDescriptorByNameUrl", old); + Stapler.getCurrentRequest2().setAttribute("currentDescriptorByNameUrl", old); } public static List getRequestHeaders(String name) { List r = new ArrayList<>(); - Enumeration e = Stapler.getCurrentRequest().getHeaders(name); + Enumeration e = Stapler.getCurrentRequest2().getHeaders(name); while (e.hasMoreElements()) { r.add(e.nextElement().toString()); } @@ -2299,13 +2407,17 @@ public static String humanReadableByteSize(long size) { double number = size; if (number >= 1024) { number = number / 1024; - measure = "KB"; + measure = "KiB"; if (number >= 1024) { number = number / 1024; - measure = "MB"; + measure = "MiB"; if (number >= 1024) { number = number / 1024; - measure = "GB"; + measure = "GiB"; + if (number >= 1024) { + number = number / 1024; + measure = "TiB"; + } } } } @@ -2335,6 +2447,7 @@ public static String breakableString(final String plain) { * Advertises the minimum set of HTTP headers that assist programmatic * discovery of Jenkins. */ + @SuppressFBWarnings(value = "UC_USELESS_VOID_METHOD", justification = "TODO needs triage") public static void advertiseHeaders(HttpServletResponse rsp) { Jenkins j = Jenkins.getInstanceOrNull(); if (j != null) { @@ -2344,6 +2457,14 @@ public static void advertiseHeaders(HttpServletResponse rsp) { } } + /** + * @deprecated use {@link #advertiseHeaders(HttpServletResponse)} + */ + @Deprecated + public static void advertiseHeaders(javax.servlet.http.HttpServletResponse rsp) { + advertiseHeaders(HttpServletResponseWrapper.toJakartaHttpServletResponse(rsp)); + } + @Restricted(NoExternalUse.class) // for actions.jelly and ContextMenu.add public static boolean isContextMenuVisible(Action a) { if (a instanceof ModelObjectWithContextMenu.ContextMenuVisibility) { @@ -2383,7 +2504,7 @@ public static Icon tryGetIcon(String iconGuess) { } private static @NonNull String filterIconNameClasses(@NonNull String classNames) { - return Arrays.stream(StringUtils.split(classNames, ' ')) + return Arrays.stream(classNames.split(" ")) .filter(className -> className.startsWith("icon-")) .collect(Collectors.joining(" ")); } @@ -2418,7 +2539,7 @@ public static String tryGetIconPath(String iconGuess, JellyContext context) { return iconGuess; } - StaplerRequest currentRequest = Stapler.getCurrentRequest(); + StaplerRequest2 currentRequest = Stapler.getCurrentRequest2(); String rootURL = currentRequest.getContextPath(); Icon iconMetadata = tryGetIcon(iconGuess); diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index e6ba431691fb..195671b1b42e 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -55,6 +55,7 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.agents.ControllerToAgentCallable; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; import jenkins.tasks.filters.EnvVarsFilterLocalRule; @@ -1114,8 +1115,7 @@ public Proc launch(ProcStarter ps) throws IOException { final String workDir = psPwd == null ? null : psPwd.getRemote(); try { - RemoteLaunchCallable remote = new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener); - remote.setEnvVarsFilterRuleWrapper(envVarsFilterRuleWrapper); + RemoteLaunchCallable remote = new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener, envVarsFilterRuleWrapper); // reset the rules to prevent build step without rules configuration to re-use those envVarsFilterRuleWrapper = null; return new ProcImpl(getChannel().call(remote)); @@ -1334,46 +1334,13 @@ public interface RemoteProcess { IOTriplet getIOtriplet(); } - private static class RemoteLaunchCallable extends MasterToSlaveCallable { - private final @NonNull List cmd; - private final @CheckForNull boolean[] masks; - private final @CheckForNull String[] env; - private final @CheckForNull InputStream in; - private final @CheckForNull OutputStream out; - private final @CheckForNull OutputStream err; - private final @CheckForNull String workDir; - private final @NonNull TaskListener listener; - private final @CheckForNull TaskListener stdoutListener; - private final boolean reverseStdin, reverseStdout, reverseStderr; - private final boolean quiet; - - private EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper; - - RemoteLaunchCallable(@NonNull List cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env, + private record RemoteLaunchCallable(@NonNull List cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env, @CheckForNull InputStream in, boolean reverseStdin, @CheckForNull OutputStream out, boolean reverseStdout, @CheckForNull OutputStream err, boolean reverseStderr, - boolean quiet, @CheckForNull String workDir, @NonNull TaskListener listener, @CheckForNull TaskListener stdoutListener) { - this.cmd = new ArrayList<>(cmd); - this.masks = masks; - this.env = env; - this.in = in; - this.out = out; - this.err = err; - this.workDir = workDir; - this.listener = listener; - this.stdoutListener = stdoutListener; - this.reverseStdin = reverseStdin; - this.reverseStdout = reverseStdout; - this.reverseStderr = reverseStderr; - this.quiet = quiet; - } - - @Restricted(NoExternalUse.class) - public void setEnvVarsFilterRuleWrapper(EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) { - this.envVarsFilterRuleWrapper = envVarsFilterRuleWrapper; - } - + boolean quiet, @CheckForNull String workDir, + @NonNull TaskListener listener, @CheckForNull TaskListener stdoutListener, + @CheckForNull EnvVarsFilterRuleWrapper envVarsFilterRuleWrapper) implements ControllerToAgentCallable { @Override public RemoteProcess call() throws IOException { final Channel channel = getOpenChannelOrFail(); @@ -1433,8 +1400,6 @@ public IOTriplet getIOtriplet() { } }); } - - private static final long serialVersionUID = 1L; } private static class RemoteChannelLaunchCallable extends MasterToSlaveCallable { diff --git a/core/src/main/java/hudson/LocalPluginManager.java b/core/src/main/java/hudson/LocalPluginManager.java index d1fcfd678e3a..7ee8c68b40d3 100644 --- a/core/src/main/java/hudson/LocalPluginManager.java +++ b/core/src/main/java/hudson/LocalPluginManager.java @@ -26,11 +26,12 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import io.jenkins.servlet.ServletContextWrapper; +import jakarta.servlet.ServletContext; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.logging.Logger; -import javax.servlet.ServletContext; import jenkins.model.Jenkins; import jenkins.util.SystemProperties; @@ -49,12 +50,20 @@ public LocalPluginManager(@CheckForNull ServletContext context, @NonNull File ro super(context, new File(rootDir, "plugins")); } + /** + * @deprecated use {@link #LocalPluginManager(ServletContext, File)} + */ + @Deprecated + public LocalPluginManager(@CheckForNull javax.servlet.ServletContext context, @NonNull File rootDir) { + this(context != null ? ServletContextWrapper.toJakartaServletContext(context) : null, rootDir); + } + /** * Creates a new LocalPluginManager * @param jenkins Jenkins instance that will use the plugin manager. */ public LocalPluginManager(@NonNull Jenkins jenkins) { - this(jenkins.servletContext, jenkins.getRootDir()); + this(jenkins.getServletContext(), jenkins.getRootDir()); } /** @@ -62,7 +71,7 @@ public LocalPluginManager(@NonNull Jenkins jenkins) { * @param rootDir Jenkins home directory. */ public LocalPluginManager(@NonNull File rootDir) { - this(null, rootDir); + this((ServletContext) null, rootDir); } @Override diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java index 625fef0c52ee..97bd9dfa8471 100644 --- a/core/src/main/java/hudson/Main.java +++ b/core/src/main/java/hudson/Main.java @@ -126,10 +126,10 @@ public static int remotePost(String[] args) throws Exception { } // get a crumb to pass the csrf check - String crumbField = null, crumbValue = null; + String crumbField = null, crumbValue = null, sessionCookies = null; try { HttpURLConnection con = open(new URL(home + - "crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)'")); + "crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)")); if (auth != null) con.setRequestProperty("Authorization", auth); String line = IOUtils.readFirstLine(con.getInputStream(), "UTF-8"); String[] components = line.split(":"); @@ -137,6 +137,7 @@ public static int remotePost(String[] args) throws Exception { crumbField = components[0]; crumbValue = components[1]; } + sessionCookies = con.getHeaderField("Set-Cookie"); } catch (IOException e) { // presumably this Hudson doesn't use CSRF protection } @@ -173,10 +174,12 @@ public static int remotePost(String[] args) throws Exception { if (auth != null) con.setRequestProperty("Authorization", auth); if (crumbField != null && crumbValue != null) { con.setRequestProperty(crumbField, crumbValue); + con.setRequestProperty("Cookie", sessionCookies); } con.setDoOutput(true); // this tells HttpURLConnection not to buffer the whole thing con.setFixedLengthStreamingMode((int) tmpFile.length()); + con.setRequestProperty("Content-Type", "application/xml"); con.connect(); // send the data try (InputStream in = Files.newInputStream(tmpFile.toPath())) { diff --git a/core/src/main/java/hudson/Plugin.java b/core/src/main/java/hudson/Plugin.java index c87a2cbe9b3c..065b604095ad 100644 --- a/core/src/main/java/hudson/Plugin.java +++ b/core/src/main/java/hudson/Plugin.java @@ -33,25 +33,29 @@ import hudson.model.Saveable; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; +import io.jenkins.servlet.ServletExceptionWrapper; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import jenkins.model.Loadable; +import jenkins.security.stapler.StaplerNotDispatchable; import jenkins.util.SystemProperties; import net.sf.json.JSONObject; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerResponse2; /** * Base class of Hudson plugin. @@ -92,11 +96,11 @@ public abstract class Plugin implements Loadable, Saveable, StaplerProxy { /** * You do not need to create custom subtypes: *

    - *
  • {@code config.jelly}, {@link #configure(StaplerRequest, JSONObject)}, {@link #load}, and {@link #save} + *
  • {@code config.jelly}, {@link #configure(StaplerRequest2, JSONObject)}, {@link #load}, and {@link #save} * can be replaced by {@link GlobalConfiguration} *
  • {@link #start} and {@link #postInitialize} can be replaced by {@link Initializer} (or {@link ItemListener#onLoaded}) *
  • {@link #stop} can be replaced by {@link Terminator} - *
  • {@link #setServletContext} can be replaced by {@link Jenkins#servletContext} + *
  • {@link #setServletContext} can be replaced by {@link Jenkins#getServletContext} *
* Note that every plugin gets a {@link DummyImpl} by default, * which will still route the URL space, serve {@link #getWrapper}, and so on. @@ -189,10 +193,10 @@ public void stop() throws Exception { /** * @since 1.233 - * @deprecated as of 1.305 override {@link #configure(StaplerRequest,JSONObject)} instead + * @deprecated as of 1.305 override {@link #configure(StaplerRequest2,JSONObject)} instead */ @Deprecated - public void configure(JSONObject formData) throws IOException, ServletException, FormException { + public void configure(JSONObject formData) throws IOException, javax.servlet.ServletException, FormException { } /** @@ -220,16 +224,60 @@ public void configure(JSONObject formData) throws IOException, ServletException, *

* If you are using this method, you'll likely be interested in * using {@link #save()} and {@link #load()}. + * @since 2.475 + */ + public void configure(StaplerRequest2 req, JSONObject formData) throws IOException, ServletException, FormException { + try { + if (Util.isOverridden(Plugin.class, getClass(), "configure", StaplerRequest.class, JSONObject.class)) { + configure(StaplerRequest.fromStaplerRequest2(req), formData); + } else { + configure(formData); + } + } catch (javax.servlet.ServletException e) { + throw ServletExceptionWrapper.toJakartaServletException(e); + } + } + + /** + * @deprecated use {@link #configure(StaplerRequest2, JSONObject)} * @since 1.305 */ - public void configure(StaplerRequest req, JSONObject formData) throws IOException, ServletException, FormException { + @Deprecated + public void configure(StaplerRequest req, JSONObject formData) throws IOException, javax.servlet.ServletException, FormException { configure(formData); } /** * This method serves static resources in the plugin under {@code hudson/plugin/SHORTNAME}. + * + * @since 2.475 + */ + public void doDynamic(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { + if (Util.isOverridden(Plugin.class, getClass(), "doDynamic", StaplerRequest.class, StaplerResponse.class)) { + try { + doDynamic(StaplerRequest.fromStaplerRequest2(req), StaplerResponse.fromStaplerResponse2(rsp)); + } catch (javax.servlet.ServletException e) { + throw ServletExceptionWrapper.toJakartaServletException(e); + } + } else { + doDynamicImpl(req, rsp); + } + } + + /** + * @deprecated use {@link #doDynamic(StaplerRequest2, StaplerResponse2)} */ - public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + @Deprecated + @StaplerNotDispatchable + public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, javax.servlet.ServletException { + try { + doDynamicImpl(StaplerRequest.toStaplerRequest2(req), StaplerResponse.toStaplerResponse2(rsp)); + } catch (ServletException e) { + throw ServletExceptionWrapper.fromJakartaServletException(e); + } + } + + private void doDynamicImpl(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { String path = req.getRestOfPath(); String pathUC = path.toUpperCase(Locale.ENGLISH); 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 649cc045b482..b90b2eb9f6d4 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -41,6 +41,7 @@ import hudson.init.InitMilestone; import hudson.init.InitStrategy; import hudson.init.InitializerFinder; +import hudson.lifecycle.Lifecycle; import hudson.model.AbstractItem; import hudson.model.AbstractModelObject; import hudson.model.AdministrativeMonitor; @@ -65,6 +66,10 @@ import hudson.util.Service; import hudson.util.VersionNumber; import hudson.util.XStream2; +import io.jenkins.servlet.ServletContextWrapper; +import io.jenkins.servlet.ServletExceptionWrapper; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FilenameFilter; @@ -90,6 +95,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; @@ -116,8 +122,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; @@ -132,19 +136,21 @@ import jenkins.model.Jenkins; import jenkins.plugins.DetachedPluginsUtil; import jenkins.security.CustomClassFilter; +import jenkins.security.stapler.StaplerNotDispatchable; import jenkins.util.SystemProperties; import jenkins.util.io.OnMaster; import jenkins.util.xml.RestrictiveEntityResolver; import net.sf.json.JSONArray; import net.sf.json.JSONObject; -import org.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.jakarta.servlet5.JakartaServletDiskFileUpload; +import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletFileUpload; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.LogFactory; import org.jenkinsci.Symbol; import org.jvnet.hudson.reactor.Executable; @@ -162,7 +168,8 @@ import org.kohsuke.stapler.StaplerOverridable; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -234,11 +241,22 @@ PluginManager doCreate(@NonNull Class klass, return klass.getConstructor(Jenkins.class).newInstance(jenkins); } }, + SC_FILE2 { + @Override + @NonNull PluginManager doCreate(@NonNull Class klass, + @NonNull Jenkins jenkins) throws ReflectiveOperationException { + return klass.getConstructor(ServletContext.class, File.class).newInstance(jenkins.getServletContext(), jenkins.getRootDir()); + } + }, + /** + * @deprecated use {@link #SC_FILE2} + */ + @Deprecated SC_FILE { @Override @NonNull PluginManager doCreate(@NonNull Class klass, @NonNull Jenkins jenkins) throws ReflectiveOperationException { - return klass.getConstructor(ServletContext.class, File.class).newInstance(jenkins.servletContext, jenkins.getRootDir()); + return klass.getConstructor(javax.servlet.ServletContext.class, File.class).newInstance(jenkins.servletContext, jenkins.getRootDir()); } }, FILE { @@ -272,7 +290,7 @@ PluginManager doCreate(@NonNull Class klass, */ public static @NonNull PluginManager createDefault(@NonNull Jenkins jenkins) { String pmClassName = SystemProperties.getString(CUSTOM_PLUGIN_MANAGER); - if (!StringUtils.isBlank(pmClassName)) { + if (pmClassName != null && !pmClassName.isBlank()) { LOGGER.log(FINE, String.format("Use of custom plugin manager [%s] requested.", pmClassName)); try { final Class klass = Class.forName(pmClassName).asSubclass(PluginManager.class); @@ -360,6 +378,9 @@ PluginManager doCreate(@NonNull Class klass, */ private final PluginStrategy strategy; + /** + * @since 2.475 + */ protected PluginManager(ServletContext context, File rootDir) { this.context = context; @@ -370,11 +391,19 @@ protected PluginManager(ServletContext context, File rootDir) { throw new UncheckedIOException(e); } String workDir = SystemProperties.getString(PluginManager.class.getName() + ".workDir"); - this.workDir = StringUtils.isBlank(workDir) ? null : new File(workDir); + this.workDir = workDir == null || workDir.isBlank() ? null : new File(workDir); strategy = createPluginStrategy(); } + /** + * @deprecated use {@link #PluginManager(ServletContext, File)} + */ + @Deprecated + protected PluginManager(javax.servlet.ServletContext context, File rootDir) { + this(context != null ? ServletContextWrapper.toJakartaServletContext(context) : null, rootDir); + } + public Api getApi() { Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); return new Api(this); @@ -612,7 +641,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 +653,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) { @@ -652,7 +681,7 @@ void considerDetachedPlugin(String shortName) { protected @NonNull Set loadPluginsFromWar(@NonNull String fromPath, @CheckForNull FilenameFilter filter) { Set names = new HashSet<>(); - ServletContext context = Jenkins.get().servletContext; + ServletContext context = Jenkins.get().getServletContext(); Set plugins = Util.fixNull(context.getResourcePaths(fromPath)); Set copiedPlugins = new HashSet<>(); Set dependencies = new HashSet<>(); @@ -713,10 +742,14 @@ 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(","); - ServletContext context = Jenkins.get().servletContext; + ServletContext context = Jenkins.get().getServletContext(); for (String dependencyToken : dependencyTokens) { if (dependencyToken.endsWith(";resolution:=optional")) { @@ -924,6 +957,9 @@ public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull 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; } @@ -1429,16 +1472,16 @@ public HttpResponse doPluginsSearch(@QueryParameter String query, @QueryParamete for (UpdateSite site : Jenkins.get().getUpdateCenter().getSiteList()) { List sitePlugins = site.getAvailables().stream() .filter(plugin -> { - if (StringUtils.isBlank(query)) { + if (query == null || query.isBlank()) { return true; } - return StringUtils.containsIgnoreCase(plugin.name, query) || - StringUtils.containsIgnoreCase(plugin.title, query) || - StringUtils.containsIgnoreCase(plugin.excerpt, query) || + 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 -> StringUtils.containsIgnoreCase(category, query)) || + .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)) @@ -1468,7 +1511,7 @@ public HttpResponse doPluginsSearch(@QueryParameter String query, @QueryParamete jsonObject.put("title", plugin.title); jsonObject.put("displayName", plugin.getDisplayName()); if (plugin.wiki == null || !(plugin.wiki.startsWith("https://") || plugin.wiki.startsWith("http://"))) { - jsonObject.put("wiki", StringUtils.EMPTY); + jsonObject.put("wiki", ""); } else { jsonObject.put("wiki", plugin.wiki); } @@ -1580,7 +1623,7 @@ public HttpResponse doPlugins() { } @RequirePOST - public HttpResponse doUpdateSources(StaplerRequest req) throws IOException { + public HttpResponse doUpdateSources(StaplerRequest2 req) throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); if (req.hasParameter("remove")) { @@ -1615,7 +1658,7 @@ public void doInstallPluginsDone() { * Performs the installation of the plugins. */ @RequirePOST - public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + public void doInstall(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); Set plugins = new LinkedHashSet<>(); @@ -1639,12 +1682,12 @@ public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOExceptio * @param req The request object. * @return A JSON response that includes a "correlationId" in the "data" element. * That "correlationId" can then be used in calls to - * {@link UpdateCenter#doInstallStatus(org.kohsuke.stapler.StaplerRequest)}. + * {@link UpdateCenter#doInstallStatus(org.kohsuke.stapler.StaplerRequest2)}. * @throws IOException Error reading JSON payload fro request. */ @RequirePOST @Restricted(DoNotUse.class) // WebOnly - public HttpResponse doInstallPlugins(StaplerRequest req) throws IOException { + public HttpResponse doInstallPlugins(StaplerRequest2 req) throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); String payload = IOUtils.toString(req.getInputStream(), req.getCharacterEncoding()); JSONObject request = JSONObject.fromObject(payload); @@ -1798,18 +1841,12 @@ public HttpResponse doSiteConfigure(@QueryParameter String site) throws IOExcept } @POST - public HttpResponse doProxyConfigure(StaplerRequest req) throws IOException, ServletException { + public HttpResponse doProxyConfigure(StaplerRequest2 req) throws IOException, ServletException { Jenkins jenkins = Jenkins.get(); jenkins.checkPermission(Jenkins.ADMINISTER); ProxyConfiguration pc = req.bindJSON(ProxyConfiguration.class, req.getSubmittedForm()); - if (pc.name == null) { - jenkins.proxy = null; - ProxyConfiguration.getXmlFile().delete(); - } else { - jenkins.proxy = pc; - jenkins.proxy.save(); - } + ProxyConfigurationManager.saveProxyConfiguration(pc); return new HttpRedirect("advanced"); } @@ -1827,13 +1864,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); + } } } @@ -1861,18 +1906,44 @@ public void cleanup() { * Uploads a plugin. */ @RequirePOST - public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, ServletException { + public HttpResponse doUploadPlugin(StaplerRequest2 req) throws IOException, ServletException { + if (Util.isOverridden(PluginManager.class, getClass(), "doUploadPlugin", StaplerRequest.class)) { + try { + return doUploadPlugin(StaplerRequest.fromStaplerRequest2(req)); + } catch (javax.servlet.ServletException e) { + throw ServletExceptionWrapper.toJakartaServletException(e); + } + } else { + return doUploadPluginImpl(req); + } + } + + /** + * @deprecated use {@link #doUploadPlugin(StaplerRequest2)} + */ + @Deprecated + @StaplerNotDispatchable + public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, javax.servlet.ServletException { + try { + return doUploadPluginImpl(StaplerRequest.toStaplerRequest2(req)); + } catch (ServletException e) { + throw ServletExceptionWrapper.fromJakartaServletException(e); + } + } + + private HttpResponse doUploadPluginImpl(StaplerRequest2 req) throws IOException, ServletException { try { Jenkins.get().checkPermission(Jenkins.ADMINISTER); 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); - if (StringUtils.isNotBlank(items.get(1).getString())) { + JakartaServletFileUpload upload = new JakartaServletDiskFileUpload(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 - fileName = items.get(1).getString(); + fileName = string; copier = new UrlPluginCopier(fileName); } else { // this is a file upload @@ -1915,7 +1986,7 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl } String deps = m.getMainAttributes().getValue("Plugin-Dependencies"); - if (StringUtils.isNotBlank(deps)) { + if (deps != null && !deps.isBlank()) { // now we get to parse it! String[] plugins = deps.split(","); for (String p : plugins) { @@ -1945,8 +2016,8 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl } @Restricted(NoExternalUse.class) - @RequirePOST public FormValidation doCheckPluginUrl(StaplerRequest request, @QueryParameter String value) throws IOException { - if (StringUtils.isNotBlank(value)) { + @RequirePOST public FormValidation doCheckPluginUrl(StaplerRequest2 request, @QueryParameter String value) throws IOException { + if (value != null && !value.isBlank()) { try { URL url = new URL(value); if (!url.getProtocol().startsWith("http")) { @@ -1964,7 +2035,7 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl } @Restricted(NoExternalUse.class) - @RequirePOST public FormValidation doCheckUpdateSiteUrl(StaplerRequest request, @QueryParameter String value) throws InterruptedException { + @RequirePOST public FormValidation doCheckUpdateSiteUrl(StaplerRequest2 request, @QueryParameter String value) throws InterruptedException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); return checkUpdateSiteURL(value); } @@ -2197,7 +2268,7 @@ private void logPluginWarnings(Map.Entry requestedPlugin, } /** - * Like {@link #doInstallNecessaryPlugins(StaplerRequest)} but only checks if everything is installed + * Like {@link #doInstallNecessaryPlugins(StaplerRequest2)} but only checks if everything is installed * or if some plugins need updates or installation. * * This method runs without side-effect. I'm still requiring the ADMINISTER permission since @@ -2207,7 +2278,7 @@ private void logPluginWarnings(Map.Entry requestedPlugin, * @since 1.483 */ @RequirePOST - public JSONArray doPrevalidateConfig(StaplerRequest req) throws IOException { + public JSONArray doPrevalidateConfig(StaplerRequest2 req) throws IOException { Jenkins.get().checkPermission(Jenkins.ADMINISTER); JSONArray response = new JSONArray(); @@ -2232,7 +2303,7 @@ public JSONArray doPrevalidateConfig(StaplerRequest req) throws IOException { * @since 1.483 */ @RequirePOST - public HttpResponse doInstallNecessaryPlugins(StaplerRequest req) throws IOException { + public HttpResponse doInstallNecessaryPlugins(StaplerRequest2 req) throws IOException { prevalidateConfig(req.getInputStream()); return HttpResponses.redirectViaContextPath("pluginManager/updates/"); } @@ -2331,7 +2402,7 @@ public static final class UberClassLoader extends ClassLoader { } public UberClassLoader(List activePlugins) { - super(PluginManager.class.getClassLoader()); + super("UberClassLoader", PluginManager.class.getClassLoader()); this.activePlugins = activePlugins; } @@ -2397,6 +2468,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/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 2161e501089f..c8cd9f5240a0 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -73,7 +73,6 @@ import jenkins.plugins.DetachedPluginsUtil; import jenkins.security.UpdateSiteWarningsMonitor; import jenkins.util.URLClassLoader2; -import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.LogFactory; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; @@ -81,8 +80,8 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -497,7 +496,12 @@ public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL @Override public String getDisplayName() { - return StringUtils.removeStart(getLongName(), "Jenkins "); + String displayName = getLongName(); + String removePrefix = "Jenkins "; + if (displayName != null && displayName.startsWith(removePrefix)) { + return displayName.substring(removePrefix.length()); + } + return displayName; } public Api getApi() { @@ -1208,7 +1212,7 @@ public PluginWrapper getPlugin(String shortName) { /** * Depending on whether the user said "dismiss" or "correct", send him to the right place. */ - public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException { + public void doAct(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException { if (req.hasParameter("correct")) { rsp.sendRedirect(req.getContextPath() + "/pluginManager"); @@ -1355,7 +1359,7 @@ public HttpResponse doDoUninstall() throws IOException { // Redo who depends on who. jenkins.getPluginManager().resolveDependentPlugins(); - return HttpResponses.redirectViaContextPath("/pluginManager/installed"); // send back to plugin manager + return HttpResponses.redirectViaContextPath("/manage/pluginManager/installed"); // send back to plugin manager } @Restricted(DoNotUse.class) // Jelly diff --git a/core/src/main/java/hudson/ProxyConfiguration.java b/core/src/main/java/hudson/ProxyConfiguration.java index 08f7131289b2..cc5b30d956dc 100644 --- a/core/src/main/java/hudson/ProxyConfiguration.java +++ b/core/src/main/java/hudson/ProxyConfiguration.java @@ -248,7 +248,7 @@ public void setTestUrl(String testUrl) { @DataBoundSetter public void setUserName(String userName) { - this.userName = userName; + this.userName = Util.fixEmptyAndTrim(userName); } @DataBoundSetter @@ -290,6 +290,7 @@ private Object readResolve() { secretPassword = Secret.fromString(Scrambler.descramble(password)); password = null; authenticator = newAuthenticator(); + userName = Util.fixEmptyAndTrim(userName); return this; } @@ -363,12 +364,9 @@ public static InputStream getInputStream(URL url) throws IOException { * *

Equivalent to {@code newHttpClientBuilder().followRedirects(HttpClient.Redirect.NORMAL).build()}. * - *

The Jenkins-specific default settings include a proxy server and proxy authentication (as - * configured by {@link ProxyConfiguration}) and a connection timeout (as configured by {@link - * ProxyConfiguration#DEFAULT_CONNECT_TIMEOUT_MILLIS}). - * * @return a new {@link HttpClient} * @since 2.379 + * @see #newHttpClientBuilder */ public static HttpClient newHttpClient() { return newHttpClientBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); @@ -383,6 +381,12 @@ public static HttpClient newHttpClient() { * configured by {@link ProxyConfiguration}) and a connection timeout (as configured by {@link * ProxyConfiguration#DEFAULT_CONNECT_TIMEOUT_MILLIS}). * + *

Warning: if both {@link #getName} and {@link #getUserName} are set + * (meaning that an authenticated proxy is defined), + * you will not be able to pass an {@code Authorization} header to the real server + * when running on Java 17 and later + * (pending JDK-8326949. + * * @return an {@link HttpClient.Builder} * @since 2.379 */ @@ -536,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( @@ -576,8 +608,8 @@ public FormValidation doValidateProxy( } try { HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.discarding()); - if (httpResponse.statusCode() == HttpURLConnection.HTTP_OK) { - return FormValidation.ok(Messages.ProxyConfiguration_Success()); + if (httpResponse.statusCode() < HttpURLConnection.HTTP_BAD_REQUEST) { + return FormValidation.ok(Messages.ProxyConfiguration_Success(httpResponse.statusCode())); } return FormValidation.error(Messages.ProxyConfiguration_FailedToConnect(testUrl, httpResponse.statusCode())); } catch (IOException e) { diff --git a/core/src/main/java/hudson/ProxyConfigurationManager.java b/core/src/main/java/hudson/ProxyConfigurationManager.java new file mode 100644 index 000000000000..d3ae0d79ee0e --- /dev/null +++ b/core/src/main/java/hudson/ProxyConfigurationManager.java @@ -0,0 +1,72 @@ +/* + * The MIT License + * + * Copyright (c) 2023, CloudBees Inc, and other contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Descriptor; +import java.io.IOException; +import jenkins.model.GlobalConfiguration; +import jenkins.model.Jenkins; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.StaplerRequest2; + +@Extension @Restricted(NoExternalUse.class) +public class ProxyConfigurationManager extends GlobalConfiguration { + + @NonNull + @Override + public String getDisplayName() { + return Messages.ProxyConfigurationManager_DisplayName(); + } + + public Descriptor getProxyDescriptor() { + return Jenkins.get().getDescriptor(ProxyConfiguration.class); + } + + @Override + public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException { + ProxyConfiguration pc = req.bindJSON(ProxyConfiguration.class, json); + try { + saveProxyConfiguration(pc); + } catch (IOException e) { + throw new FormException(e.getMessage(), e, null); + } + return true; + } + + public static void saveProxyConfiguration(ProxyConfiguration pc) throws IOException { + Jenkins jenkins = Jenkins.get(); + if (pc.name == null) { + jenkins.proxy = null; + ProxyConfiguration.getXmlFile().delete(); + } else { + jenkins.proxy = pc; + jenkins.proxy.save(); + } + } + +} diff --git a/core/src/main/java/hudson/ResponseHeaderFilter.java b/core/src/main/java/hudson/ResponseHeaderFilter.java index 90fb0be87d37..416e5c4c6919 100644 --- a/core/src/main/java/hudson/ResponseHeaderFilter.java +++ b/core/src/main/java/hudson/ResponseHeaderFilter.java @@ -24,15 +24,15 @@ package hudson; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Enumeration; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; +import org.kohsuke.stapler.CompatibleFilter; /** * This filter allows you to modify headers set by the container or other servlets @@ -77,7 +77,7 @@ * * @author Mike Wille */ -public class ResponseHeaderFilter implements Filter { +public class ResponseHeaderFilter implements CompatibleFilter { private FilterConfig config; @Override diff --git a/core/src/main/java/hudson/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 3936071de2ef..e7f037652c9b 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -271,14 +271,11 @@ public void run() { String protocol = s.substring(9); AgentProtocol p = AgentProtocol.of(protocol); if (p != null) { - if (Jenkins.get().getAgentProtocols().contains(protocol)) { - LOGGER.log(p instanceof PingAgentProtocol ? Level.FINE : Level.INFO, () -> "Accepted " + protocol + " connection " + connectionInfo); - p.handle(this.s); - } else { - error("Disabled protocol:" + s, this.s); - } - } else + LOGGER.log(p instanceof PingAgentProtocol ? Level.FINE : Level.INFO, () -> "Accepted " + protocol + " connection " + connectionInfo); + p.handle(this.s); + } else { error("Unknown protocol:", this.s); + } } else { error("Unrecognized protocol: " + s, this.s); } @@ -364,21 +361,11 @@ public PingAgentProtocol() { ping = "Ping\n".getBytes(StandardCharsets.UTF_8); } - @Override - public boolean isRequired() { - return true; - } - @Override public String getName() { return "Ping"; } - @Override - public String getDisplayName() { - return Messages.TcpSlaveAgentListener_PingAgentProtocol_displayName(); - } - @Override public void handle(Socket socket) throws IOException, InterruptedException { try (socket) { diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index c4d1e7088336..07d00a44d006 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -43,6 +43,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; +import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.io.Writer; @@ -83,6 +84,8 @@ import java.text.ParseException; import java.time.LocalDate; import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; @@ -108,6 +111,7 @@ import java.util.regex.Pattern; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import jenkins.model.Jenkins; import jenkins.util.MemoryReductionUtil; import jenkins.util.SystemProperties; import jenkins.util.io.PathRemover; @@ -121,6 +125,7 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; /** * Various utility methods that don't have more proper home. @@ -1520,6 +1525,10 @@ public static Number tryParseNumber(@CheckForNull String numberStr, @CheckForNul * does not contain the specified method. */ public static boolean isOverridden(@NonNull Class base, @NonNull Class derived, @NonNull String methodName, @NonNull Class... types) { + if (base == derived) { + // If base and derived are the same type, the method is not overridden by definition + return false; + } // If derived is not a subclass or implementor of base, it can't override any method // Technically this should also be triggered when base == derived, because it can't override its own method, but // the unit tests explicitly test for that as working. @@ -1846,9 +1855,11 @@ public static long daysElapsedSince(@NonNull Date date) { /** * Find the specific ancestor, or throw an exception. * Useful for an ancestor we know is inside the URL to ease readability + * + * @since 2.475 */ @Restricted(NoExternalUse.class) - public static @NonNull T getNearestAncestorOfTypeOrThrow(@NonNull StaplerRequest request, @NonNull Class clazz) { + public static @NonNull T getNearestAncestorOfTypeOrThrow(@NonNull StaplerRequest2 request, @NonNull Class clazz) { T t = request.findAncestorObject(clazz); if (t == null) { throw new IllegalArgumentException("No ancestor of type " + clazz.getName() + " in the request"); @@ -1856,9 +1867,43 @@ public static long daysElapsedSince(@NonNull Date date) { return t; } + /** + * @deprecated use {@link #getNearestAncestorOfTypeOrThrow(StaplerRequest2, Class)} + */ + @Deprecated + @Restricted(NoExternalUse.class) + public static @NonNull T getNearestAncestorOfTypeOrThrow(@NonNull StaplerRequest request, @NonNull Class clazz) { + return getNearestAncestorOfTypeOrThrow(StaplerRequest.toStaplerRequest2(request), clazz); + } + + @Restricted(NoExternalUse.class) + public static void printRedirect(String contextPath, String redirectUrl, String message, PrintWriter out) { + out.printf( + "" + + "" + + "" + + "" + + "%n" + + "%2$s%n" + + " - + @@ -53,12 +53,14 @@ THE SOFTWARE. icon="symbol-chevron-down" tooltip="${null}" clazz="jenkins-button--primary"> - + + + diff --git a/core/src/main/resources/hudson/PluginManager/available_sv_SE.properties b/core/src/main/resources/hudson/PluginManager/available_sv_SE.properties new file mode 100644 index 000000000000..8c963861efc8 --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/available_sv_SE.properties @@ -0,0 +1,13 @@ +# This file is under the MIT License by authors + +Install=Installera +Install\ after\ restart=Installera efter omstart +Released=Utgivning +Search\ available\ plugins=Sök efter tillgängliga insticksprogram +Install\ without\ restart=Installera utan omstart +Installed=Installerad +Download\ now\ and\ install\ after\ restart=Ladda ned nu och installera efter omstart +Plugin\ Manager=Insticksprogramshanterare +Plugins=Insticksprogram +Available\ plugins=Tillgängliga insticksprogram +Name=Namn diff --git a/core/src/main/resources/hudson/PluginManager/check_sv_SE.properties b/core/src/main/resources/hudson/PluginManager/check_sv_SE.properties index 87e2c89db6ee..fdc30054d02e 100644 --- a/core/src/main/resources/hudson/PluginManager/check_sv_SE.properties +++ b/core/src/main/resources/hudson/PluginManager/check_sv_SE.properties @@ -20,4 +20,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +lastUpdated=Uppdateringinformation hämtades: {0} sedan Check\ now=Kontrollera nu diff --git a/core/src/main/resources/hudson/PluginManager/index_sv_SE.properties b/core/src/main/resources/hudson/PluginManager/index_sv_SE.properties index b9e54cb65a36..17dd4d16447d 100644 --- a/core/src/main/resources/hudson/PluginManager/index_sv_SE.properties +++ b/core/src/main/resources/hudson/PluginManager/index_sv_SE.properties @@ -23,4 +23,6 @@ All=Alla None=Inga Select=Välj -UpdatePageDescription=Den här sidan visar uppdateringar på insticksmoduler som du använder. +UpdatePageDescription=Den här sidan visar uppdateringar för insticksprogram som du använder. +UpdatePageLegend=Inaktiverade rader har redan uppgraderats och väntar på omstart. \ + Skuggade men markerbara rader pågår eller har misslyckats. diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 8013742cf738..c9b7dd93f200 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}