From bb0725000d7ceac8d5e75c4bf16fe948520aac1d Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 31 Jan 2025 21:52:07 +0000 Subject: [PATCH 1/7] Coding Standards: Replace loose comparison in `wp_xmlrpc_server::wp_setOptions()`. Follow-up to [8114]. Props aristath, poena, afercia, SergeyBiryukov. See #62279. git-svn-id: https://develop.svn.wordpress.org/trunk@59748 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-xmlrpc-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 81dc57f7d1e15..a406b63105d6e 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -4330,7 +4330,7 @@ public function wp_setOptions( $args ) { continue; } - if ( true == $this->blog_options[ $o_name ]['readonly'] ) { + if ( $this->blog_options[ $o_name ]['readonly'] ) { continue; } From 1111166932c228163f512ae47e559a4fda6aa806 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 1 Feb 2025 20:15:09 +0000 Subject: [PATCH 2/7] Build/Test Tools: Parallelise the performance tests. This change introduces a job matrix for the "current", "before", and "base" performance tests to replace the current behaviour of running them sequentially in a single job. This speeds up the overall performance testing workflow and also reduces the chance of any given test interfering with another, for example by making a change to data in the database that affects a subsequent test. Props johnbillion, swissspidy, dmsnell, joemcgill. See #62221 git-svn-id: https://develop.svn.wordpress.org/trunk@59749 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/performance.yml | 68 ++++- .../reusable-performance-report-v2.yml | 114 ++++++++ .../reusable-performance-test-v2.yml | 270 ++++++++++++++++++ 3 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/reusable-performance-report-v2.yml create mode 100644 .github/workflows/reusable-performance-test-v2.yml diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index fc29f0c015ee1..27339c1eefc41 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -33,6 +33,7 @@ on: # Confirm any changes to relevant workflow files. - '.github/workflows/performance.yml' - '.github/workflows/reusable-performance.yml' + - '.github/workflows/reusable-performance-*.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. @@ -47,21 +48,82 @@ concurrency: permissions: {} jobs: + determine-matrix: + name: Determine Matrix + runs-on: ubuntu-24.04 + if: ${{ ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) && ! contains( github.event.before, '00000000' ) }} + permissions: {} + env: + TARGET_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + outputs: + subjects: ${{ steps.set-subjects.outputs.result }} + target_sha: ${{ env.TARGET_SHA }} + steps: + # The `workflow_dispatch` event is the only one missing the needed SHA to target. + - name: Retrieve previous commit SHA (if necessary) + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "TARGET_SHA=$(git rev-parse HEAD^1)" >> "$GITHUB_ENV" + + - name: Set subjects + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: set-subjects + with: + script: | + const artifacts = await github.rest.actions.listArtifactsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wordpress-build-' + process.env.TARGET_SHA, + }); + const has_previous_build = !! artifacts.data.artifacts[0]; + + const subjects = [ + 'current', + ]; + + if ( context.eventName === 'push' && context.ref === 'refs/heads/trunk' ) { + subjects.push( 'base' ); + } else if ( has_previous_build ) { + subjects.push( 'before' ); + } + + return subjects; + # Runs the performance test suite. performance: - name: ${{ matrix.multisite && 'Multisite' || 'Single site' }} - uses: ./.github/workflows/reusable-performance.yml + name: ${{ matrix.multisite && 'Multisite' || 'Single Site' }} ${{ matrix.memcached && 'Memcached' || 'Default' }} + uses: ./.github/workflows/reusable-performance-test-v2.yml + needs: [ determine-matrix ] + permissions: + contents: read + strategy: + fail-fast: false + matrix: + memcached: [ true, false ] + multisite: [ true, false ] + subject: ${{ fromJson( needs.determine-matrix.outputs.subjects ) }} + with: + memcached: ${{ matrix.memcached }} + multisite: ${{ matrix.multisite }} + subject: ${{ matrix.subject }} + TARGET_SHA: ${{ needs.determine-matrix.outputs.target_sha }} + + compare: + name: ${{ matrix.label }} + uses: ./.github/workflows/reusable-performance-report-v2.yml + needs: [ determine-matrix, performance ] permissions: contents: read - if: ${{ ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: fail-fast: false matrix: memcached: [ true, false ] multisite: [ true, false ] + label: [ Compare ] with: memcached: ${{ matrix.memcached }} multisite: ${{ matrix.multisite }} + BASE_TAG: ${{ needs.performance.outputs.BASE_TAG }} + publish: ${{ contains( fromJson( needs.determine-matrix.outputs.subjects ), 'base' ) && ! matrix.memcached && ! matrix.multisite }} secrets: CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} diff --git a/.github/workflows/reusable-performance-report-v2.yml b/.github/workflows/reusable-performance-report-v2.yml new file mode 100644 index 0000000000000..880945541e4e8 --- /dev/null +++ b/.github/workflows/reusable-performance-report-v2.yml @@ -0,0 +1,114 @@ +## +# A reusable workflow that compares and publishes the performance tests. +## +name: Compare and publish performance Tests + +on: + workflow_call: + inputs: + BASE_TAG: + description: 'The version being used for baseline measurements.' + required: true + type: string + memcached: + description: 'Whether to enable memcached.' + required: false + type: boolean + default: false + multisite: + description: 'Whether to use Multisite.' + required: false + type: boolean + default: false + publish: + description: 'Whether to publish the results to Code Vitals.' + required: false + type: boolean + default: false + secrets: + CODEVITALS_PROJECT_TOKEN: + description: 'The authorization token for https://www.codevitals.run/project/wordpress.' + required: false + +env: + BASE_TAG: ${{ inputs.BASE_TAG }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Performs the following steps: + # - Checkout repository. + # - Set up Node.js. + # - Download the relevant performance test artifacts. + # - List the downloaded files for debugging. + # - Compare results. + # - Add workflow summary. + # - Determine the sha of the baseline tag if necessary. + # - Publish performance results if necessary. + compare: + name: ${{ inputs.multisite && 'Multisite' || 'Single Site' }} ${{ inputs.memcached && 'Memcached' || 'Default' }} ${{ inputs.publish && '(Publishes)' || '' }} + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + fetch-depth: ${{ github.event_name == 'workflow_dispatch' && '2' || '1' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Download artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + pattern: performance-${{ inputs.multisite && 'multisite' || 'single' }}-${{ inputs.memcached && 'memcached' || 'default' }}-* + path: artifacts + merge-multiple: true + + - name: List files + run: tree artifacts + + - name: Compare results + run: node ./tests/performance/compare-results.js "${RUNNER_TEMP}/summary.md" + + - name: Add workflow summary + run: cat "${RUNNER_TEMP}/summary.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Set the base sha + # Only needed when publishing results. + if: ${{ inputs.publish }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: base-sha + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const baseRef = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/' + process.env.BASE_TAG, + }); + return baseRef.data.object.sha; + + - name: Publish performance results + if: ${{ inputs.publish }} + env: + BASE_SHA: ${{ steps.base-sha.outputs.result }} + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} + HOST_NAME: www.codevitals.run + run: | + if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then + echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" + exit 1 + fi + COMMITTED_AT="$(git show -s "$GITHUB_SHA" --format='%cI')" + node ./tests/performance/log-results.js "$CODEVITALS_PROJECT_TOKEN" trunk "$GITHUB_SHA" "$BASE_SHA" "$COMMITTED_AT" "$HOST_NAME" diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml new file mode 100644 index 0000000000000..421bb129ebe12 --- /dev/null +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -0,0 +1,270 @@ +## +# A reusable workflow that runs the performance test suite. +## +name: Run performance Tests + +on: + workflow_call: + inputs: + subject: + description: Subject of the test. One of `current`, `before`, or `base`. + required: true + type: string + LOCAL_DIR: + description: 'Where to run WordPress from.' + required: false + type: 'string' + default: 'build' + BASE_TAG: + description: 'The version being used for baseline measurements.' + required: false + type: 'string' + default: '6.7.0' + TARGET_SHA: + description: 'SHA hash of the target commit used for "before" measurements.' + required: true + type: 'string' + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + memcached: + description: 'Whether to enable memcached.' + required: false + type: 'boolean' + default: false + multisite: + description: 'Whether to use Multisite.' + required: false + type: 'boolean' + default: false + outputs: + BASE_TAG: + description: 'The version being used for baseline measurements.' + value: ${{ inputs.BASE_TAG }} + secrets: + CODEVITALS_PROJECT_TOKEN: + description: 'The authorization token for https://www.codevitals.run/project/wordpress.' + required: false + +env: + PUPPETEER_SKIP_DOWNLOAD: ${{ true }} + + # Prevent wp-scripts from downloading extra Playwright browsers, + # since Chromium will be installed in its dedicated step already. + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + + # Performance testing should be performed in an environment reflecting a standard production environment. + LOCAL_WP_DEBUG: false + LOCAL_SCRIPT_DEBUG: false + LOCAL_SAVEQUERIES: false + LOCAL_WP_DEVELOPMENT_MODE: "''" + + BASE_TAG: ${{ inputs.BASE_TAG }} + TARGET_SHA: ${{ inputs.TARGET_SHA }} + + LOCAL_DIR: ${{ inputs.LOCAL_DIR }} + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + LOCAL_PHP: ${{ inputs.php-version }}${{ 'latest' != inputs.php-version && '-fpm' || '' }} + LOCAL_MULTISITE: ${{ inputs.multisite }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Performs the following steps: + # - Configure environment variables. + # - Checkout repository. + # - Set up Node.js. + # - Log debug information. + # - Install npm dependencies. + # - Install Playwright browsers. + # - Build WordPress. + # - Start Docker environment. + # - Put the baseline or target version of WordPress in place if necessary. + # - Install object cache drop-in. + # - Log running Docker containers. + # - Docker debug information. + # - Install WordPress. + # - WordPress debug information. + # - Enable themes on Multisite. + # - Install WordPress Importer plugin. + # - Import mock data. + # - Deactivate WordPress Importer plugin. + # - Update permalink structure. + # - Install additional languages. + # - Disable external HTTP requests. + # - Disable cron. + # - List defined constants. + # - Install MU plugin. + # - Run performance tests. + # - Archive artifacts. + # - Ensure version-controlled files are not modified or deleted. + performance: + name: Test ${{ inputs.subject == 'base' && inputs.BASE_TAG || inputs.subject }} + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + fetch-depth: ${{ github.event_name == 'workflow_dispatch' && '2' || '1' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + locale -a + + - name: Install npm dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Start Docker environment + run: npm run env:start + + - name: Build WordPress + run: npm run build + + - name: Download previous build artifact (target branch or previous commit) + if: ${{ inputs.subject == 'before' }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: get-previous-build + with: + script: | + const artifacts = await github.rest.actions.listArtifactsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wordpress-build-' + process.env.TARGET_SHA, + }); + const matchArtifact = artifacts.data.artifacts[0]; + if ( ! matchArtifact ) { + core.setFailed( 'No artifact found!' ); + return false; + } + const download = await github.rest.actions.downloadArtifact( { + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + } ); + const fs = require( 'fs' ); + fs.writeFileSync( process.env.GITHUB_WORKSPACE + '/before.zip', Buffer.from( download.data ) ) + return true; + + - name: Unzip the previous build + if: ${{ inputs.subject == 'before' }} + run: | + unzip "${GITHUB_WORKSPACE}/before.zip" + unzip -o "${GITHUB_WORKSPACE}/wordpress.zip" + + - name: Set the environment to the baseline version + if: ${{ inputs.subject == 'base' }} + run: | + VERSION="${BASE_TAG%.0}" + npm run env:cli -- core download --version="$VERSION" --force --path="/var/www/${LOCAL_DIR}" + + - name: Install object cache drop-in + if: ${{ inputs.memcached }} + run: cp src/wp-content/object-cache.php build/wp-content/object-cache.php + + - name: Log running Docker containers + run: docker ps -a + + - name: Docker debug information + run: | + docker -v + docker compose run --rm mysql mysql --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + run: npm run env:install + + - name: Check version number + run: npm run env:cli -- core version --path="/var/www/${LOCAL_DIR}" + + - name: Enable themes on Multisite + if: ${{ inputs.multisite }} + run: | + npm run env:cli -- theme enable twentytwentyone --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentythree --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentyfour --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentyfive --network --path="/var/www/${LOCAL_DIR}" + + - name: Install WordPress Importer plugin + run: npm run env:cli -- plugin install wordpress-importer --activate --path="/var/www/${LOCAL_DIR}" + + - name: Import mock data + run: | + curl -O https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml + npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path="/var/www/${LOCAL_DIR}" + rm themeunittestdata.wordpress.xml + + - name: Deactivate WordPress Importer plugin + run: npm run env:cli -- plugin deactivate wordpress-importer --path="/var/www/${LOCAL_DIR}" + + - name: Update permalink structure + run: npm run env:cli -- rewrite structure '/%year%/%monthnum%/%postname%/' --path="/var/www/${LOCAL_DIR}" + + - name: Install additional languages + run: | + npm run env:cli -- language core install de_DE --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language plugin install de_DE --all --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language theme install de_DE --all --path="/var/www/${LOCAL_DIR}" + + # Prevent background update checks from impacting test stability. + - name: Disable external HTTP requests + run: npm run env:cli -- config set WP_HTTP_BLOCK_EXTERNAL true --raw --type=constant --path="/var/www/${LOCAL_DIR}" + + # Prevent background tasks from impacting test stability. + - name: Disable cron + run: npm run env:cli -- config set DISABLE_WP_CRON true --raw --type=constant --path="/var/www/${LOCAL_DIR}" + + - name: List defined constants + run: npm run env:cli -- config list --path="/var/www/${LOCAL_DIR}" + + - name: Install MU plugin + run: | + mkdir "./${LOCAL_DIR}/wp-content/mu-plugins" + cp ./tests/performance/wp-content/mu-plugins/server-timing.php "./${LOCAL_DIR}/wp-content/mu-plugins/server-timing.php" + + - name: Run performance tests + run: npm run test:performance + env: + TEST_RESULTS_PREFIX: ${{ inputs.subject != 'current' && inputs.subject || '' }} + + - name: Archive artifacts + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + if: always() + with: + name: performance-${{ inputs.multisite && 'multisite' || 'single' }}-${{ inputs.memcached && 'memcached' || 'default' }}-${{ inputs.subject }} + path: artifacts + if-no-files-found: error + include-hidden-files: true + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code From 4180ca6425e8113f80991efd432a0fbe173bf29b Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sat, 1 Feb 2025 20:50:13 +0000 Subject: [PATCH 3/7] Coding Standards: Use strict comparison in some legacy media functions. Follow-up to [7062], [8653], [12188]. Props aristath, poena, afercia, SergeyBiryukov. See #62279. git-svn-id: https://develop.svn.wordpress.org/trunk@59750 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/media.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 4484d1672519e..0f988a2dac62b 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -93,7 +93,7 @@ function the_media_upload_tabs() { foreach ( $tabs as $callback => $text ) { $class = ''; - if ( $current == $callback ) { + if ( $current === $callback ) { $class = " class='current'"; } @@ -1172,7 +1172,7 @@ function image_align_input_fields( $post, $checked = '' ) { foreach ( $alignments as $name => $label ) { $name = esc_attr( $name ); $output[] = ""; } @@ -1222,7 +1222,7 @@ function image_size_input_fields( $post, $check = '' ) { $css_id = "image-size-{$size}-{$post->ID}"; // If this size is the default but that's not available, don't select it. - if ( $size == $check ) { + if ( $size === $check ) { if ( $enabled ) { $checked = " checked='checked'"; } else { From bcdca3f9925f1d3eca7b78d231837c0caf0c8c24 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Sun, 2 Feb 2025 11:15:55 +0000 Subject: [PATCH 4/7] Coding Standards: Use strict comparison in `get_media_item()`. Follow-up to [12081], [12351]. Props aristath, poena, afercia, SergeyBiryukov. See #62279. git-svn-id: https://develop.svn.wordpress.org/trunk@59751 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 0f988a2dac62b..24dfef62f5b8a 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -1762,7 +1762,7 @@ function get_media_item( $attachment_id, $args = null ) { if ( 'image' === $type && $calling_post_id && current_theme_supports( 'post-thumbnails', get_post_type( $calling_post_id ) ) && post_type_supports( get_post_type( $calling_post_id ), 'thumbnail' ) - && get_post_thumbnail_id( $calling_post_id ) != $attachment_id + && get_post_thumbnail_id( $calling_post_id ) !== $attachment_id ) { $calling_post = get_post( $calling_post_id ); From 3ff586a4e38681389d51206245750e5b72887cf6 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 3 Feb 2025 13:35:25 +0000 Subject: [PATCH 5/7] Build/Test Tools: Fix the source code path handling when installing the local development environment. This ensures the correct code is used to run the installation depending on whether it should be running from the `src` or `build` directory. Props swissspidy, johnbillion See #62221 git-svn-id: https://develop.svn.wordpress.org/trunk@59752 602fd350-edb4-49c9-b593-d223f7449a82 --- tools/local-env/scripts/install.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index d924ffc30deba..f8e6285fb9a33 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -12,7 +12,7 @@ dotenvExpand.expand( dotenv.config() ); local_env_utils.determine_auth_option(); // Create wp-config.php. -wp_cli( 'config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --path=/var/www/src --force' ); +wp_cli( 'config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force' ); // Add the debug settings to wp-config.php. // Windows requires this to be done as an additional step, rather than using the --extra-php option in the previous step. @@ -24,7 +24,7 @@ wp_cli( `config set WP_ENVIRONMENT_TYPE ${process.env.LOCAL_WP_ENVIRONMENT_TYPE} wp_cli( `config set WP_DEVELOPMENT_MODE ${process.env.LOCAL_WP_DEVELOPMENT_MODE} --type=constant` ); // Move wp-config.php to the base directory, so it doesn't get mixed up in the src or build directories. -renameSync( 'src/wp-config.php', 'wp-config.php' ); +renameSync( `${process.env.LOCAL_DIR}/wp-config.php`, 'wp-config.php' ); install_wp_importer(); @@ -55,7 +55,7 @@ wait_on( { resources: [ `tcp:localhost:${process.env.LOCAL_PORT}`] } ) function wp_cli( cmd ) { const composeFiles = local_env_utils.get_compose_files(); - execSync( `docker compose ${composeFiles} run --quiet-pull --rm cli ${cmd}`, { stdio: 'inherit' } ); + execSync( `docker compose ${composeFiles} run --quiet-pull --rm cli ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio: 'inherit' } ); } /** From 8711aa5ab60e35d78dab73e316674b59b90246da Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 3 Feb 2025 16:53:16 +0000 Subject: [PATCH 6/7] Posts, Post Types: Explicitly pass a redirect URL for the post permalink when submitting the post password form. This allows the subsequent redirect to behave as expected if a site is using a strict referrer policy on the front end which prevents the full referrer from being sent. Props zodiac1978, yogeshbhutkar, hbhalodia, mukesh27. Fixes #62881 git-svn-id: https://develop.svn.wordpress.org/trunk@59753 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/post-template.php | 10 +++++++++- src/wp-login.php | 13 +++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/post-template.php b/src/wp-includes/post-template.php index 598ea3d60865d..15dfc67d6f7a1 100644 --- a/src/wp-includes/post-template.php +++ b/src/wp-includes/post-template.php @@ -1780,6 +1780,7 @@ function get_the_password_form( $post = 0 ) { $invalid_password_html = ''; $aria = ''; $class = ''; + $redirect_field = ''; // If the referrer is the same as the current request, the user has entered an invalid password. if ( ! empty( $post->ID ) && wp_get_raw_referer() === get_permalink( $post->ID ) && isset( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ) ) { @@ -1798,7 +1799,14 @@ function get_the_password_form( $post = 0 ) { $class = ' password-form-error'; } - $output = '
' . $invalid_password_html . ' + if ( ! empty( $post->ID ) ) { + $redirect_field = sprintf( + '', + esc_attr( get_permalink( $post->ID ) ) + ); + } + + $output = '' . $redirect_field . $invalid_password_html . '

' . __( 'This content is password protected. To view it please enter your password below:' ) . '

'; diff --git a/src/wp-login.php b/src/wp-login.php index c1b06bbc3bac7..0d824dea655ea 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -764,8 +764,10 @@ function wp_login_viewport_meta() { break; case 'postpass': + $redirect_to = $_POST['redirect_to'] ?? wp_get_referer(); + if ( ! isset( $_POST['post_password'] ) || ! is_string( $_POST['post_password'] ) ) { - wp_safe_redirect( wp_get_referer() ); + wp_safe_redirect( $redirect_to ); exit; } @@ -782,18 +784,17 @@ function wp_login_viewport_meta() { * * @param int $expires The expiry time, as passed to setcookie(). */ - $expire = apply_filters( 'post_password_expires', time() + 10 * DAY_IN_SECONDS ); - $referer = wp_get_referer(); + $expire = apply_filters( 'post_password_expires', time() + 10 * DAY_IN_SECONDS ); - if ( $referer ) { - $secure = ( 'https' === parse_url( $referer, PHP_URL_SCHEME ) ); + if ( $redirect_to ) { + $secure = ( 'https' === parse_url( $redirect_to, PHP_URL_SCHEME ) ); } else { $secure = false; } setcookie( 'wp-postpass_' . COOKIEHASH, $hasher->HashPassword( wp_unslash( $_POST['post_password'] ) ), $expire, COOKIEPATH, COOKIE_DOMAIN, $secure ); - wp_safe_redirect( wp_get_referer() ); + wp_safe_redirect( $redirect_to ); exit; case 'logout': From 85d00ec54c3a552ca28a996f6e1209e45821358a Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Mon, 3 Feb 2025 19:50:50 +0000 Subject: [PATCH 7/7] Security: Add the `SensitiveParameter` attribute to sensitive parameters. Values passed to parameters with this attribute will be redacted if present in a stack trace when using PHP 8.2 or later. This reduces the chance that passwords and security keys get accidentally exposed in debug logs and bug reports. Props petitphp, TobiasBg, jrf, johnbillion. Fixes #57304 git-svn-id: https://develop.svn.wordpress.org/trunk@59754 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-importer.php | 8 ++- src/wp-admin/includes/upgrade.php | 19 ++++++- .../class-wp-application-passwords.php | 5 +- src/wp-includes/class-wp-xmlrpc-server.php | 12 ++++- src/wp-includes/class-wpdb.php | 8 ++- src/wp-includes/ms-functions.php | 54 ++++++++++++++++--- src/wp-includes/pluggable-deprecated.php | 17 +++++- src/wp-includes/pluggable.php | 24 +++++++-- .../class-wp-rest-users-controller.php | 7 ++- src/wp-includes/user.php | 53 +++++++++++++++--- 10 files changed, 178 insertions(+), 29 deletions(-) diff --git a/src/wp-admin/includes/class-wp-importer.php b/src/wp-admin/includes/class-wp-importer.php index eca3199a7b5c7..8edef7dba58de 100644 --- a/src/wp-admin/includes/class-wp-importer.php +++ b/src/wp-admin/includes/class-wp-importer.php @@ -195,7 +195,13 @@ public function cmpr_strlen( $a, $b ) { * @param bool $head * @return array */ - public function get_page( $url, $username = '', $password = '', $head = false ) { + public function get_page( + $url, + $username = '', + #[\SensitiveParameter] + $password = '', + $head = false + ) { // Increase the timeout. add_filter( 'http_request_timeout', array( $this, 'bump_request_timeout' ) ); diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index 068e28b040e33..94f40cda318a1 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -44,7 +44,16 @@ * @type string $password_message The explanatory message regarding the password. * } */ - function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecated = '', $user_password = '', $language = '' ) { + function wp_install( + $blog_title, + $user_name, + $user_email, + $is_public, + $deprecated = '', + #[\SensitiveParameter] + $user_password = '', + $language = '' + ) { if ( ! empty( $deprecated ) ) { _deprecated_argument( __FUNCTION__, '2.6.0' ); } @@ -563,7 +572,13 @@ function wp_install_maybe_enable_pretty_permalinks() { * @param string $password Administrator's password. Note that a placeholder message is * usually passed instead of the actual password. */ - function wp_new_blog_notification( $blog_title, $blog_url, $user_id, $password ) { + function wp_new_blog_notification( + $blog_title, + $blog_url, + $user_id, + #[\SensitiveParameter] + $password + ) { $user = new WP_User( $user_id ); $email = $user->user_email; $name = $user->user_login; diff --git a/src/wp-includes/class-wp-application-passwords.php b/src/wp-includes/class-wp-application-passwords.php index b76b5c7e2a5ae..43965790594b1 100644 --- a/src/wp-includes/class-wp-application-passwords.php +++ b/src/wp-includes/class-wp-application-passwords.php @@ -459,7 +459,10 @@ protected static function set_user_application_passwords( $user_id, $passwords ) * @param string $raw_password The raw application password. * @return string The chunked password. */ - public static function chunk_password( $raw_password ) { + public static function chunk_password( + #[\SensitiveParameter] + $raw_password + ) { $raw_password = preg_replace( '/[^a-z\d]/i', '', $raw_password ); return trim( chunk_split( $raw_password, 4, ' ' ) ); diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index a406b63105d6e..e69d0eb395acf 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -285,7 +285,11 @@ public function addTwoNumbers( $args ) { * @param string $password User's password. * @return WP_User|false WP_User object if authentication passed, false otherwise. */ - public function login( $username, $password ) { + public function login( + $username, + #[\SensitiveParameter] + $password + ) { if ( ! $this->is_enabled ) { $this->error = new IXR_Error( 405, sprintf( __( 'XML-RPC services are disabled on this site.' ) ) ); return false; @@ -330,7 +334,11 @@ public function login( $username, $password ) { * @param string $password User's password. * @return bool Whether authentication passed. */ - public function login_pass_ok( $username, $password ) { + public function login_pass_ok( + $username, + #[\SensitiveParameter] + $password + ) { return (bool) $this->login( $username, $password ); } diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 69b934ae38ed2..5ace90d490e17 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -749,7 +749,13 @@ class wpdb { * @param string $dbname Database name. * @param string $dbhost Database host. */ - public function __construct( $dbuser, $dbpassword, $dbname, $dbhost ) { + public function __construct( + $dbuser, + #[\SensitiveParameter] + $dbpassword, + $dbname, + $dbhost + ) { if ( WP_DEBUG && WP_DEBUG_DISPLAY ) { $this->show_errors(); } diff --git a/src/wp-includes/ms-functions.php b/src/wp-includes/ms-functions.php index 404eb1497a7cc..1f890dab69232 100644 --- a/src/wp-includes/ms-functions.php +++ b/src/wp-includes/ms-functions.php @@ -938,7 +938,16 @@ function wpmu_signup_user( $user, $user_email, $meta = array() ) { * @param array $meta Optional. Signup meta data. By default, contains the requested privacy setting and lang_id. * @return bool */ -function wpmu_signup_blog_notification( $domain, $path, $title, $user_login, $user_email, $key, $meta = array() ) { +function wpmu_signup_blog_notification( + $domain, + $path, + $title, + $user_login, + $user_email, + #[\SensitiveParameter] + $key, + $meta = array() +) { /** * Filters whether to bypass the new site email notification. * @@ -1073,7 +1082,13 @@ function wpmu_signup_blog_notification( $domain, $path, $title, $user_login, $us * @param array $meta Optional. Signup meta data. Default empty array. * @return bool */ -function wpmu_signup_user_notification( $user_login, $user_email, $key, $meta = array() ) { +function wpmu_signup_user_notification( + $user_login, + $user_email, + #[\SensitiveParameter] + $key, + $meta = array() +) { /** * Filters whether to bypass the email notification for new user sign-up. * @@ -1175,7 +1190,10 @@ function wpmu_signup_user_notification( $user_login, $user_email, $key, $meta = * @param string $key The activation key provided to the user. * @return array|WP_Error An array containing information about the activated user and/or blog. */ -function wpmu_activate_signup( $key ) { +function wpmu_activate_signup( + #[\SensitiveParameter] + $key +) { global $wpdb; $signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE activation_key = %s", $key ) ); @@ -1327,7 +1345,12 @@ function wp_delete_signup_on_user_delete( $id, $reassign, $user ) { * @param string $email The new user's email address. * @return int|false Returns false on failure, or int $user_id on success. */ -function wpmu_create_user( $user_name, $password, $email ) { +function wpmu_create_user( + $user_name, + #[\SensitiveParameter] + $password, + $email +) { $user_name = preg_replace( '/\s+/', '', sanitize_user( $user_name, true ) ); $user_id = wp_create_user( $user_name, $password, $email ); @@ -1611,7 +1634,14 @@ function domain_exists( $domain, $path, $network_id = 1 ) { * @param array $meta Optional. Signup meta data. By default, contains the requested privacy setting and lang_id. * @return bool Whether the email notification was sent. */ -function wpmu_welcome_notification( $blog_id, $user_id, $password, $title, $meta = array() ) { +function wpmu_welcome_notification( + $blog_id, + $user_id, + #[\SensitiveParameter] + $password, + $title, + $meta = array() +) { $current_network = get_network(); /** @@ -1845,7 +1875,12 @@ function wpmu_new_site_admin_notification( $site_id, $user_id ) { * @param array $meta Optional. Signup meta data. Default empty array. * @return bool */ -function wpmu_welcome_user_notification( $user_id, $password, $meta = array() ) { +function wpmu_welcome_user_notification( + $user_id, + #[\SensitiveParameter] + $password, + $meta = array() +) { $current_network = get_network(); /** @@ -2271,7 +2306,12 @@ function add_existing_user_to_blog( $details = false ) { * @param string $password User password. Ignored. * @param array $meta Signup meta data. */ -function add_new_user_to_blog( $user_id, $password, $meta ) { +function add_new_user_to_blog( + $user_id, + #[\SensitiveParameter] + $password, + $meta +) { if ( ! empty( $meta['add_to_blog'] ) ) { $blog_id = $meta['add_to_blog']; $role = $meta['new_role']; diff --git a/src/wp-includes/pluggable-deprecated.php b/src/wp-includes/pluggable-deprecated.php index 11e393ddfdb84..11ccad2e8a57b 100644 --- a/src/wp-includes/pluggable-deprecated.php +++ b/src/wp-includes/pluggable-deprecated.php @@ -101,7 +101,15 @@ function get_user_by_email($email) { * @param string $siteurl Optional. Will be used instead of SITECOOKIEPATH if set * @param bool $remember Optional. Remember that the user is logged in */ -function wp_setcookie($username, $password = '', $already_md5 = false, $home = '', $siteurl = '', $remember = false) { +function wp_setcookie( + $username, + #[\SensitiveParameter] + $password = '', + $already_md5 = false, + $home = '', + $siteurl = '', + $remember = false +) { _deprecated_function( __FUNCTION__, '2.5.0', 'wp_set_auth_cookie()' ); $user = get_user_by('login', $username); wp_set_auth_cookie($user->ID, $remember); @@ -168,7 +176,12 @@ function wp_get_cookie_login() { * @param string $deprecated Not used * @return bool True on successful check, false on login failure. */ -function wp_login($username, $password, $deprecated = '') { +function wp_login( + $username, + #[\SensitiveParameter] + $password, + $deprecated = '' +) { _deprecated_function( __FUNCTION__, '2.5.0', 'wp_signon()' ); global $error; diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index f4a8d8412e88c..60ad0841ed8ce 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -598,7 +598,11 @@ function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() * @return WP_User|WP_Error WP_User object if the credentials are valid, * otherwise WP_Error. */ - function wp_authenticate( $username, $password ) { + function wp_authenticate( + $username, + #[\SensitiveParameter] + $password + ) { $username = sanitize_user( $username ); $password = trim( $password ); @@ -2631,7 +2635,10 @@ function wp_hash( $data, $scheme = 'auth', $algo = 'md5' ) { * @param string $password Plain text user password to hash. * @return string The hash string of the password. */ - function wp_hash_password( $password ) { + function wp_hash_password( + #[\SensitiveParameter] + $password + ) { global $wp_hasher; if ( empty( $wp_hasher ) ) { @@ -2667,7 +2674,12 @@ function wp_hash_password( $password ) { * @param string|int $user_id Optional. User ID. * @return bool False, if the $password does not match the hashed password. */ - function wp_check_password( $password, $hash, $user_id = '' ) { + function wp_check_password( + #[\SensitiveParameter] + $password, + $hash, + $user_id = '' + ) { global $wp_hasher; // If the hash is still md5... @@ -2863,7 +2875,11 @@ function wp_rand( $min = null, $max = null ) { * @param string $password The plaintext new user password. * @param int $user_id User ID. */ - function wp_set_password( $password, $user_id ) { + function wp_set_password( + #[\SensitiveParameter] + $password, + $user_id + ) { global $wpdb; $old_user_data = get_userdata( $user_id ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index 84bcea052cf32..64578c7810b7b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -1310,7 +1310,12 @@ public function check_username( $value, $request, $param ) { * @param string $param The parameter name. * @return string|WP_Error The sanitized password, if valid, otherwise an error. */ - public function check_user_password( $value, $request, $param ) { + public function check_user_password( + #[\SensitiveParameter] + $value, + $request, + $param + ) { $password = (string) $value; if ( empty( $password ) ) { diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index e4e6f5b652744..ede1330251263 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -150,7 +150,12 @@ function wp_signon( $credentials = array(), $secure_cookie = '' ) { * @param string $password Password for authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ -function wp_authenticate_username_password( $user, $username, $password ) { +function wp_authenticate_username_password( + $user, + $username, + #[\SensitiveParameter] + $password +) { if ( $user instanceof WP_User ) { return $user; } @@ -228,7 +233,12 @@ function wp_authenticate_username_password( $user, $username, $password ) { * @param string $password Password for authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ -function wp_authenticate_email_password( $user, $email, $password ) { +function wp_authenticate_email_password( + $user, + $email, + #[\SensitiveParameter] + $password +) { if ( $user instanceof WP_User ) { return $user; } @@ -301,7 +311,12 @@ function wp_authenticate_email_password( $user, $email, $password ) { * @param string $password Password. If not empty, cancels the cookie authentication. * @return WP_User|WP_Error WP_User on success, WP_Error on failure. */ -function wp_authenticate_cookie( $user, $username, $password ) { +function wp_authenticate_cookie( + $user, + $username, + #[\SensitiveParameter] + $password +) { global $auth_secure_cookie; if ( $user instanceof WP_User ) { @@ -342,7 +357,12 @@ function wp_authenticate_cookie( $user, $username, $password ) { * @return WP_User|WP_Error|null WP_User on success, WP_Error on failure, null if * null is passed in and this isn't an API request. */ -function wp_authenticate_application_password( $input_user, $username, $password ) { +function wp_authenticate_application_password( + $input_user, + $username, + #[\SensitiveParameter] + $password +) { if ( $input_user instanceof WP_User ) { return $input_user; } @@ -2846,7 +2866,12 @@ function wp_update_user( $userdata ) { * @return int|WP_Error The newly created user's ID or a WP_Error object if the user could not * be created. */ -function wp_create_user( $username, $password, $email = '' ) { +function wp_create_user( + $username, + #[\SensitiveParameter] + $password, + $email = '' +) { $user_login = wp_slash( $username ); $user_email = wp_slash( $email ); $user_pass = $password; @@ -3034,7 +3059,11 @@ function get_password_reset_key( $user ) { * @param string $login The user login. * @return WP_User|WP_Error WP_User object on success, WP_Error object for invalid or expired keys. */ -function check_password_reset_key( $key, $login ) { +function check_password_reset_key( + #[\SensitiveParameter] + $key, + $login +) { global $wp_hasher; $key = preg_replace( '/[^a-z0-9]/i', '', $key ); @@ -3371,7 +3400,11 @@ function retrieve_password( $user_login = '' ) { * @param WP_User $user The user * @param string $new_pass New password for the user in plaintext */ -function reset_password( $user, $new_pass ) { +function reset_password( + $user, + #[\SensitiveParameter] + $new_pass +) { /** * Fires before the user's password is reset. * @@ -4932,7 +4965,11 @@ function wp_generate_user_request_key( $request_id ) { * @param string $key Provided key to validate. * @return true|WP_Error True on success, WP_Error on failure. */ -function wp_validate_user_request_key( $request_id, $key ) { +function wp_validate_user_request_key( + $request_id, + #[\SensitiveParameter] + $key +) { global $wp_hasher; $request_id = absint( $request_id );