From abf8dc0cdcf3880c6d79513fb0452c3c0dcb55e7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 27 Mar 2026 14:45:57 +0000 Subject: [PATCH 01/57] Branch 7.0. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62159 602fd350-edb4-49c9-b593-d223f7449a82 From 18c39cd99579b03e25f6704b1578676bcf0a4477 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 27 Mar 2026 15:31:44 +0000 Subject: [PATCH 02/57] Post 7.0 branching changes for the 7.0 branch. Reviewed by SergeyBiryukov. See #64966. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62162 602fd350-edb4-49c9-b593-d223f7449a82 --- .env.example | 2 +- .github/workflows/check-built-files.yml | 2 +- .github/workflows/cleanup-pull-requests.yml | 2 +- .github/workflows/coding-standards.yml | 6 +- .../workflows/commit-built-file-changes.yml | 168 --------- .github/workflows/end-to-end-tests.yml | 4 +- .github/workflows/failed-workflow.yml | 56 --- .github/workflows/install-testing.yml | 187 --------- .github/workflows/javascript-tests.yml | 4 +- .../workflows/javascript-type-checking.yml | 4 +- .../workflows/local-docker-environment.yml | 6 +- .github/workflows/performance.yml | 6 +- .github/workflows/php-compatibility.yml | 4 +- .github/workflows/phpstan-static-analysis.yml | 4 +- .github/workflows/phpunit-tests.yml | 12 +- .github/workflows/reusable-build-package.yml | 60 --- .../workflows/reusable-check-built-files.yml | 110 ------ .../reusable-cleanup-pull-requests.yml | 122 ------ .../reusable-coding-standards-javascript.yml | 61 --- .../reusable-coding-standards-php.yml | 108 ------ .../workflows/reusable-end-to-end-tests.yml | 157 -------- .../workflows/reusable-javascript-tests.yml | 71 ---- .../reusable-javascript-type-checking-v1.yml | 76 ---- .../reusable-performance-report-v2.yml | 114 ------ .../reusable-performance-test-v2.yml | 267 ------------- .github/workflows/reusable-performance.yml | 357 ------------------ .../workflows/reusable-php-compatibility.yml | 90 ----- .../reusable-phpstan-static-analysis-v1.yml | 109 ------ .../workflows/reusable-phpunit-tests-v1.yml | 197 ---------- .../workflows/reusable-phpunit-tests-v2.yml | 212 ----------- .../workflows/reusable-phpunit-tests-v3.yml | 271 ------------- .../reusable-support-json-reader-v1.yml | 155 -------- .../reusable-test-core-build-process.yml | 158 -------- .../reusable-test-gutenberg-build-process.yml | 100 ----- ...sable-test-local-docker-environment-v1.yml | 170 --------- .../workflows/reusable-upgrade-testing.yml | 137 ------- .github/workflows/reusable-workflow-lint.yml | 34 -- .github/workflows/slack-notifications.yml | 229 ----------- .../workflows/test-and-zip-default-themes.yml | 305 --------------- .github/workflows/test-build-processes.yml | 6 +- .github/workflows/test-coverage.yml | 115 ------ .github/workflows/test-old-branches.yml | 151 -------- .github/workflows/upgrade-develop-testing.yml | 8 +- .github/workflows/upgrade-testing.yml | 241 ------------ .github/workflows/workflow-lint.yml | 4 +- docker-compose.yml | 6 +- 46 files changed, 40 insertions(+), 4628 deletions(-) delete mode 100644 .github/workflows/commit-built-file-changes.yml delete mode 100644 .github/workflows/failed-workflow.yml delete mode 100644 .github/workflows/install-testing.yml delete mode 100644 .github/workflows/reusable-build-package.yml delete mode 100644 .github/workflows/reusable-check-built-files.yml delete mode 100644 .github/workflows/reusable-cleanup-pull-requests.yml delete mode 100644 .github/workflows/reusable-coding-standards-javascript.yml delete mode 100644 .github/workflows/reusable-coding-standards-php.yml delete mode 100644 .github/workflows/reusable-end-to-end-tests.yml delete mode 100644 .github/workflows/reusable-javascript-tests.yml delete mode 100644 .github/workflows/reusable-javascript-type-checking-v1.yml delete mode 100644 .github/workflows/reusable-performance-report-v2.yml delete mode 100644 .github/workflows/reusable-performance-test-v2.yml delete mode 100644 .github/workflows/reusable-performance.yml delete mode 100644 .github/workflows/reusable-php-compatibility.yml delete mode 100644 .github/workflows/reusable-phpstan-static-analysis-v1.yml delete mode 100644 .github/workflows/reusable-phpunit-tests-v1.yml delete mode 100644 .github/workflows/reusable-phpunit-tests-v2.yml delete mode 100644 .github/workflows/reusable-phpunit-tests-v3.yml delete mode 100644 .github/workflows/reusable-support-json-reader-v1.yml delete mode 100644 .github/workflows/reusable-test-core-build-process.yml delete mode 100644 .github/workflows/reusable-test-gutenberg-build-process.yml delete mode 100644 .github/workflows/reusable-test-local-docker-environment-v1.yml delete mode 100644 .github/workflows/reusable-upgrade-testing.yml delete mode 100644 .github/workflows/reusable-workflow-lint.yml delete mode 100644 .github/workflows/slack-notifications.yml delete mode 100644 .github/workflows/test-and-zip-default-themes.yml delete mode 100644 .github/workflows/test-coverage.yml delete mode 100644 .github/workflows/test-old-branches.yml delete mode 100644 .github/workflows/upgrade-testing.yml diff --git a/.env.example b/.env.example index 76a4744165505..5fc17c65f7fad 100644 --- a/.env.example +++ b/.env.example @@ -15,7 +15,7 @@ LOCAL_PORT=8889 LOCAL_DIR=src # The PHP version to use. Valid options are 'latest', and '{version}-fpm'. -LOCAL_PHP=latest +LOCAL_PHP=8.5-fpm # Whether or not to enable Xdebug. LOCAL_PHP_XDEBUG=false diff --git a/.github/workflows/check-built-files.yml b/.github/workflows/check-built-files.yml index 01a239c4eb3b0..3f9ae477c2b3d 100644 --- a/.github/workflows/check-built-files.yml +++ b/.github/workflows/check-built-files.yml @@ -49,6 +49,6 @@ jobs: check-for-built-file-changes: name: Check built files if: ${{ github.repository == 'wordpress/wordpress-develop' }} - uses: ./.github/workflows/reusable-check-built-files.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-check-built-files.yml@trunk permissions: contents: read diff --git a/.github/workflows/cleanup-pull-requests.yml b/.github/workflows/cleanup-pull-requests.yml index 578710dcf56ac..6c8d38e67b0b7 100644 --- a/.github/workflows/cleanup-pull-requests.yml +++ b/.github/workflows/cleanup-pull-requests.yml @@ -25,4 +25,4 @@ jobs: permissions: pull-requests: write if: ${{ github.repository == 'WordPress/wordpress-develop' }} - uses: ./.github/workflows/reusable-cleanup-pull-requests.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-cleanup-pull-requests.yml@trunk diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 6f9fc831df92f..9d00eb90882d8 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -52,7 +52,7 @@ jobs: # Runs the PHP coding standards checks. phpcs: name: Coding standards - uses: ./.github/workflows/reusable-coding-standards-php.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-coding-standards-php.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} @@ -60,14 +60,14 @@ jobs: # Runs the JavaScript coding standards checks. jshint: name: Coding standards - uses: ./.github/workflows/reusable-coding-standards-javascript.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-coding-standards-javascript.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/commit-built-file-changes.yml b/.github/workflows/commit-built-file-changes.yml deleted file mode 100644 index f93cd4bd662ec..0000000000000 --- a/.github/workflows/commit-built-file-changes.yml +++ /dev/null @@ -1,168 +0,0 @@ -# Commits all missed changes to built files back to pull request branches. -name: Commit Built File Changes (PRs) - -on: - workflow_run: - workflows: - - 'Check Built Files (PRs)' - - 'Test Default Themes & Create ZIPs' - types: - - completed - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'workflow_run' && format( '{0}-{1}', github.event.workflow_run.head_branch, github.event.workflow_run.head_repository.name ) || github.sha }} - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Checks a PR for uncommitted changes to built files. - # - # Performs the following steps: - # - Attempts to download the artifact containing the PR diff. - # - Checks for the existence of an artifact. - # - Unzips the artifact. - # - Generates a token for authenticating with the GitHub App. - # - Checks out the repository. - # - Applies the patch file. - # - Displays the result of git diff. - # - Configures the Git author. - # - Stages changes. - # - Commits changes. - # - Pushes changes. - update-built-files: - name: Check and update built files - runs-on: ubuntu-24.04 - if: ${{ github.repository == 'wordpress/wordpress-develop' }} - timeout-minutes: 10 - permissions: - contents: write - steps: - - name: Download artifact - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const artifacts = await github.rest.actions.listWorkflowRunArtifacts( { - owner: context.repo.owner, - repo: context.repo.repo, - run_id: process.env.RUN_ID, - } ); - - const matchArtifact = artifacts.data.artifacts.filter( ( artifact ) => { - return artifact.name === 'pr-built-file-changes' - } )[0]; - - if ( ! matchArtifact ) { - core.info( 'No artifact found!' ); - return; - } - - 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( '${{ github.workspace }}/pr-built-file-changes.zip', Buffer.from( download.data ) ) - env: - RUN_ID: ${{ github.event.workflow_run.id }} - - - name: Check for artifact - id: artifact-check - run: | - if [ -f "pr-built-file-changes.zip" ]; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Unzip the artifact containing the PR data - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - run: unzip pr-built-file-changes.zip - - - name: Generate Installation Token - id: generate_token - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - env: - GH_APP_ID: ${{ secrets.GH_PR_BUILT_FILES_APP_ID }} - GH_APP_PRIVATE_KEY: ${{ secrets.GH_PR_BUILT_FILES_PRIVATE_KEY }} - run: | - echo "$GH_APP_PRIVATE_KEY" > private-key.pem - - # Generate JWT - JWT=$(python3 - <> "$GITHUB_ENV" - - rm -f private-key.pem - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - with: - repository: ${{ github.event.workflow_run.head_repository.full_name }} - ref: ${{ github.event.workflow_run.head_branch }} - path: 'pr-repo' - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - token: ${{ env.ACCESS_TOKEN }} - - - name: Apply patch - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - working-directory: 'pr-repo' - run: git apply ${{ github.workspace }}/changes.diff - - - name: Display changes to versioned files - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - working-directory: 'pr-repo' - run: git diff - - - name: Configure git user name and email - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - working-directory: 'pr-repo' - env: - GH_APP_ID: ${{ secrets.GH_PR_BUILT_FILES_APP_ID }} - run: | - git config user.name "wordpress-develop-pr-bot[bot]" - git config user.email ${{ env.GH_APP_ID }}+wordpress-develop-pr-bot[bot]@users.noreply.github.com - - - name: Stage changes - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - working-directory: 'pr-repo' - run: git add . - - - name: Commit changes - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - working-directory: 'pr-repo' - run: | - git commit -m "Automation: Updating built files with changes." - - - name: Push changes - if: ${{ steps.artifact-check.outputs.exists == 'true' }} - working-directory: 'pr-repo' - run: git push diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index b397a2241947e..1d1c88ec5e82d 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -59,7 +59,7 @@ jobs: # Runs the end-to-end test suite. e2e-tests: name: ${{ matrix.label }} - uses: ./.github/workflows/reusable-end-to-end-tests.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-end-to-end-tests.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} @@ -74,7 +74,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/failed-workflow.yml b/.github/workflows/failed-workflow.yml deleted file mode 100644 index 6df8999464a68..0000000000000 --- a/.github/workflows/failed-workflow.yml +++ /dev/null @@ -1,56 +0,0 @@ -## -# Performs follow-up tasks when a workflow fails or is cancelled. -## -name: Failed Workflow - -on: - workflow_dispatch: - inputs: - run_id: - description: 'ID of the GitHub Action workflow run to rerun' - required: true - type: 'string' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Attempts to rerun a workflow. - # - # Performs the following steps: - # - Retrieves the workflow run that dispatched this workflow. - # - Restarts all failed jobs when the workflow fails or is cancelled for the first time. - failed-workflow: - name: Rerun a workflow - runs-on: ubuntu-24.04 - permissions: - actions: write - timeout-minutes: 30 - - steps: - - name: Rerun a workflow - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - retries: 15 - retry-exempt-status-codes: 418 - script: | - const workflow_run = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: process.env.RUN_ID, - }); - - // Only rerun after the first run attempt. - if ( workflow_run.data.run_attempt > 1 ) { - return; - } - - const rerun = await github.rest.actions.reRunWorkflowFailedJobs({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: process.env.RUN_ID, - enable_debug_logging: true - }); - env: - RUN_ID: ${{ inputs.run_id }} diff --git a/.github/workflows/install-testing.yml b/.github/workflows/install-testing.yml deleted file mode 100644 index f15d6e4830268..0000000000000 --- a/.github/workflows/install-testing.yml +++ /dev/null @@ -1,187 +0,0 @@ -# Confirms that installing WordPress using WP-CLI works successfully. -# -# This workflow is not meant to test wordpress-develop checkouts, but rather tagged versions officially available on WordPress.org. -# -# This workflow is triggered for all WordPress versions that are currently receiving security updates. It therefore needs to -# retain support for older PHP and database versions. -name: Installation Tests - -on: - push: - branches: - - trunk - # Always test the workflow after it's updated. - paths: - - '.github/workflows/install-testing.yml' - - '.version-support-*.json' - - '.github/workflows/reusable-support-json-reader-v1.yml' - pull_request: - # Always test the workflow when changes are suggested. - paths: - - '.version-support-*.json' - - '.github/workflows/install-testing.yml' - - '.github/workflows/reusable-support-json-reader-v1.yml' - - schedule: - - cron: '0 0 * * 1' - workflow_dispatch: - inputs: - wp-version: - description: 'The version to test installing. Accepts major and minor versions, "latest", or "nightly". Major releases must not end with ".0".' - type: string - default: 'nightly' - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ inputs.wp-version || github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Determines the supported values for PHP and database versions based on the WordPress version being tested. - build-test-matrix: - name: Build Test Matrix - uses: ./.github/workflows/reusable-support-json-reader-v1.yml - permissions: - contents: read - secrets: inherit - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - with: - wp-version: ${{ inputs.wp-version }} - - # Test the WordPress installation process through WP-CLI. - # - # Performs the following steps: - # - Sets up PHP. - # - Downloads the specified version of WordPress. - # - Creates a `wp-config.php` file. - # - Installs WordPress. - install-tests-mysql: - name: WP ${{ inputs.wp-version || 'nightly' }} / PHP ${{ matrix.php }} / ${{ 'mariadb' == matrix.db-type && 'MariaDB' || 'MySQL' }} ${{ matrix.db-version }}${{ matrix.multisite && ' multisite' || '' }} - permissions: - contents: read - runs-on: ${{ matrix.os }} - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - timeout-minutes: 10 - needs: [ build-test-matrix ] - strategy: - fail-fast: false - matrix: - os: [ ubuntu-24.04 ] - php: ${{ fromJSON( needs.build-test-matrix.outputs.php-versions ) }} - db-type: [ 'mysql' ] - db-version: ${{ fromJSON( needs.build-test-matrix.outputs.mysql-versions ) }} - multisite: [ false, true ] - memcached: [ false ] - - # Exclude some PHP and MySQL versions that cannot currently be tested with Docker containers. - exclude: - # There are no local WordPress Docker environment containers for PHP <= 5.3. - - php: '5.2' - - php: '5.3' - # MySQL containers <= 5.5 do not exist or fail to start properly. - - db-version: '5.0' - - db-version: '5.1' - - db-version: '5.5' - # Only test the latest innovation release. - - db-version: '9.0' - - db-version: '9.1' - - db-version: '9.2' - - db-version: '9.3' - - db-version: '9.4' - - db-version: '9.5' - # MySQL 9.0+ will not work on PHP 7.2 & 7.3. See https://core.trac.wordpress.org/ticket/61218. - - php: '7.2' - db-version: '9.6' - - php: '7.3' - db-version: '9.6' - - services: - database: - image: ${{ matrix.db-type }}:${{ matrix.db-version }} - ports: - - 3306 - options: >- - --health-cmd="mysqladmin ping" - --health-interval="30s" - --health-timeout="10s" - --health-retries="5" - -e MYSQL_ROOT_PASSWORD="root" - -e MYSQL_DATABASE="test_db" - --entrypoint sh ${{ matrix.db-type }}:${{ matrix.db-version }} - -c "exec docker-entrypoint.sh mysqld${{ matrix.db-type == 'mysql' && contains( fromJSON('["5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3"]'), matrix.php ) && ( matrix.db-version == '8.4' && ' --mysql-native-password=ON --authentication-policy=mysql_native_password' || ' --default-authentication-plugin=mysql_native_password' ) || '' }}" - - steps: - - name: Set up PHP ${{ matrix.php }} - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: '${{ matrix.php }}' - coverage: none - tools: ${{ contains( fromJSON('["5.4", "5.5"]'), matrix.php ) && 'wp-cli:2.4.0' || 'wp-cli' }} - - - name: Download WordPress - run: wp core download --version="${WP_VERSION}" - env: - WP_VERSION: ${{ inputs.wp-version || 'nightly' }} - - - name: Create wp-config.php file - run: wp config create --dbname=test_db --dbuser=root --dbpass=root --dbhost="127.0.0.1:${DB_PORT}" - env: - DB_PORT: ${{ job.services.database.ports['3306'] }} - - - name: Install WordPress - run: wp core ${{ matrix.multisite && 'multisite-install' || 'install' }} --url=http://localhost/ --title="Upgrade Test" --admin_user=admin --admin_password=password --admin_email=me@example.org --skip-email - - slack-notifications: - name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml - permissions: - actions: read - contents: read - needs: [ install-tests-mysql ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-24.04 - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: `${context.runId}`, - } - }); diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index 4ebb1fd17b499..f6aaed30a83c9 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -55,14 +55,14 @@ jobs: # Runs the WordPress Core JavaScript tests. test-js: name: QUnit Tests - uses: ./.github/workflows/reusable-javascript-tests.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-javascript-tests.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/javascript-type-checking.yml b/.github/workflows/javascript-type-checking.yml index b8a10da5465bd..2d0f054af8ff2 100644 --- a/.github/workflows/javascript-type-checking.yml +++ b/.github/workflows/javascript-type-checking.yml @@ -46,14 +46,14 @@ jobs: # Runs JavaScript type checking. typecheck: name: JavaScript type checking - uses: ./.github/workflows/reusable-javascript-type-checking-v1.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-javascript-type-checking-v1.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/local-docker-environment.yml b/.github/workflows/local-docker-environment.yml index c9dbae312595a..1adbaff8d8c8f 100644 --- a/.github/workflows/local-docker-environment.yml +++ b/.github/workflows/local-docker-environment.yml @@ -76,7 +76,7 @@ jobs: # build-test-matrix: name: Build Test Matrix - uses: ./.github/workflows/reusable-support-json-reader-v1.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-support-json-reader-v1.yml@trunk permissions: contents: read secrets: inherit @@ -87,7 +87,7 @@ jobs: # Tests the local Docker environment. environment-tests-mysql: name: PHP ${{ matrix.php }} - uses: ./.github/workflows/reusable-test-local-docker-environment-v1.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-test-local-docker-environment-v1.yml@trunk permissions: contents: read needs: [ build-test-matrix ] @@ -122,7 +122,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index d9be2c8842ec4..d1e9595cf0f06 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -97,7 +97,7 @@ jobs: # Runs the performance test suite. performance: name: ${{ matrix.multisite && 'Multisite' || 'Single Site' }} ${{ matrix.memcached && 'Memcached' || 'Default' }} - uses: ./.github/workflows/reusable-performance-test-v2.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-performance-test-v2.yml@trunk needs: [ determine-matrix ] permissions: contents: read @@ -115,7 +115,7 @@ jobs: compare: name: ${{ matrix.label }} - uses: ./.github/workflows/reusable-performance-report-v2.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-performance-report-v2.yml@trunk needs: [ determine-matrix, performance ] permissions: contents: read @@ -136,7 +136,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index bd81c8958daa6..9f47aa1002a10 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -44,14 +44,14 @@ jobs: # Runs PHP compatibility testing. php-compatibility: name: Check PHP compatibility - uses: ./.github/workflows/reusable-php-compatibility.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-php-compatibility.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/phpstan-static-analysis.yml b/.github/workflows/phpstan-static-analysis.yml index a479e8e371214..4f989122bba0a 100644 --- a/.github/workflows/phpstan-static-analysis.yml +++ b/.github/workflows/phpstan-static-analysis.yml @@ -42,14 +42,14 @@ jobs: # Runs PHPStan Static Analysis. phpstan: name: PHP static analysis - uses: ./.github/workflows/reusable-phpstan-static-analysis-v1.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-phpstan-static-analysis-v1.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index de36d5a505187..51b1988322e5e 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -63,7 +63,7 @@ jobs: # test-with-mysql: name: PHP ${{ matrix.php }} - uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-phpunit-tests-v3.yml@trunk permissions: contents: read secrets: inherit @@ -140,7 +140,7 @@ jobs: # test-with-mariadb: name: PHP ${{ matrix.php }} - uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-phpunit-tests-v3.yml@trunk permissions: contents: read secrets: inherit @@ -192,7 +192,7 @@ jobs: # test-innovation-releases: name: PHP ${{ matrix.php }} - uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-phpunit-tests-v3.yml@trunk permissions: contents: read secrets: inherit @@ -235,7 +235,7 @@ jobs: # html-api-test-groups: name: ${{ matrix.label }} - uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-phpunit-tests-v3.yml@trunk permissions: contents: read secrets: inherit @@ -264,7 +264,7 @@ jobs: # limited-matrix-for-forks: name: PHP ${{ matrix.php }} - uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-phpunit-tests-v3.yml@trunk permissions: contents: read secrets: inherit @@ -315,7 +315,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/reusable-build-package.yml b/.github/workflows/reusable-build-package.yml deleted file mode 100644 index 320ec1c621335..0000000000000 --- a/.github/workflows/reusable-build-package.yml +++ /dev/null @@ -1,60 +0,0 @@ -## -# A reusable workflow that builds and packages WordPress. The resulting package can be used to test upgrading and installing. -## -name: Build and package WordPress - -on: - workflow_call: - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Builds and packages WordPress. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Runs the build script. - # - Prepares the directory structure for the ZIP. - # - Creates a ZIP of the built files. - # - Uploads the ZIP as a GitHub Actions artifact. - build: - name: WordPress - permissions: - contents: read - runs-on: ubuntu-24.04 - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Install npm Dependencies - run: npm ci - - - name: Build WordPress - run: npm run build - - - name: Prepare the directory structure for the ZIP - run: mv build wordpress - - - name: Create ZIP of built files - run: zip -q -r develop.zip wordpress/. - - - name: Upload ZIP as a GitHub Actions artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wordpress-develop - path: develop.zip - if-no-files-found: error diff --git a/.github/workflows/reusable-check-built-files.yml b/.github/workflows/reusable-check-built-files.yml deleted file mode 100644 index 11d97639a30fc..0000000000000 --- a/.github/workflows/reusable-check-built-files.yml +++ /dev/null @@ -1,110 +0,0 @@ -## -# A reusable workflow that checks for uncommitted changes to built files in pull requests. -## -name: Check Built Files (PRs) - -on: - workflow_call: - -permissions: {} - -jobs: - # Checks a PR for uncommitted changes to built files. - # - # When changes are detected, the patch is stored as an artifact for processing by the Commit Built File Changes - # workflow. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Configures caching for Composer. - # - Installs Composer dependencies. - # - Logs general debug information about the runner. - # - Installs npm dependencies. - # - Builds CSS file using SASS. - # - Builds Emoji files. - # - Builds bundled Root Certificate files. - # - Builds WordPress. - # - Checks for changes to versioned files. - # - Displays the result of git diff for debugging purposes. - # - Saves the diff to a patch file. - # - Uploads the patch file as an artifact. - update-built-files: - name: Check and update built files - runs-on: ubuntu-24.04 - timeout-minutes: 10 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - # This date is used to ensure that the PHPCS cache is cleared at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: ${{ steps.get-date.outputs.date }} - - - name: Log debug information - run: | - npm --version - node --version - curl --version - git --version - - - name: Install npm Dependencies - run: npm ci - - - name: Run SASS precommit tasks - run: npm run grunt precommit:css - - - name: Run Emoji precommit task - run: npm run grunt precommit:emoji - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Run certificate tasks - run: npm run grunt copy:certificates - - - name: Build WordPress - run: npm run build:dev - - - name: Check for changes to versioned files - id: built-file-check - run: | - if git diff --quiet; then - echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT" - else - echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT" - fi - - - name: Display changes to versioned files - if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} - run: git diff - - - name: Save diff to a file - if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} - run: git diff > ./changes.diff - - # Uploads the diff file as an artifact. - - name: Upload diff file as artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} - with: - name: pr-built-file-changes - path: changes.diff diff --git a/.github/workflows/reusable-cleanup-pull-requests.yml b/.github/workflows/reusable-cleanup-pull-requests.yml deleted file mode 100644 index 9dae63cb213d3..0000000000000 --- a/.github/workflows/reusable-cleanup-pull-requests.yml +++ /dev/null @@ -1,122 +0,0 @@ -## -# A reusable workflow that finds and closes any pull requests that are linked to Trac -# tickets that are referenced as fixed in commit messages. -# -# More info about using GitHub pull requests for contributing to WordPress can be found in the handbook: https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/. -## -name: Run pull request cleanup - -on: - workflow_call: - -jobs: - # Finds and closes pull requests referencing fixed Trac tickets in commit messages using the - # documented expected format - # - # Commit message format is documented in the Core handbook: https://make.wordpress.org/core/handbook/best-practices/commit-messages/. - # - # Performs the following steps: - # - Parse fixed ticket numbers from the commit message. - # - Parse the SVN revision from the commit message. - # - Searches for pull requests referencing any fixed tickets. - # - Leaves a comment on each PR before closing. - close-prs: - name: Find and close PRs - runs-on: ubuntu-24.04 - permissions: - pull-requests: write - - steps: - - name: Find fixed ticket numbers - id: trac-tickets - env: - COMMIT_MSG_RAW: ${{ github.event.head_commit.message }} - run: | - COMMIT_MESSAGE="$(echo "$COMMIT_MSG_RAW" | sed -n '/^Fixes #/,/\./p')" - echo "fixed_list=$(echo "$COMMIT_MESSAGE" | sed -n 's/.*Fixes #\([0-9]\+\).*/\1/p' | tr '\n' ' ')" >> "$GITHUB_OUTPUT" - - - name: Get the SVN revision - id: git-svn-id - env: - COMMIT_MSG_RAW: ${{ github.event.head_commit.message }} - run: | - COMMIT_MESSAGE="$(echo "$COMMIT_MSG_RAW" | sed -n '$p')" - echo "svn_revision_number=$(echo "$COMMIT_MESSAGE" | sed -n 's/.*git-svn-id: https:\/\/develop.svn.wordpress.org\/[^@]*@\([0-9]*\) .*/\1/p')" >> "$GITHUB_OUTPUT" - - - name: Find pull requests - id: linked-prs - if: ${{ steps.trac-tickets.outputs.fixed_list != '' && steps.git-svn-id.outputs.svn_revision_number != '' }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const fixedList = "${{ steps.trac-tickets.outputs.fixed_list }}".split(' ').filter(Boolean); - let prNumbers = []; - - for (const ticket of fixedList) { - const tracTicketUrl = `https://core.trac.wordpress.org/ticket/${ticket}`; - const corePrefix = `core-${ticket}`; - - const query = ` - query($searchQuery: String!) { - search(query: $searchQuery, type: ISSUE_ADVANCED, first: 20) { - nodes { - ... on PullRequest { - number - bodyText - } - } - } - } - `; - - const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open ( "${tracTicketUrl}" OR "${corePrefix}" )`; - - const result = await github.graphql(query, { - searchQuery, - }); - - // Since search queries will match anywhere for any activity on a pull request, the body specifically needs to be manually checked. - const matchingPRs = result.search.nodes - .filter(pr => { - const bodyLower = pr.bodyText.toLowerCase(); - - return bodyLower.includes(tracTicketUrl.toLowerCase()) || bodyLower.includes(corePrefix.toLowerCase()); - }).map(pr => pr.number); - - prNumbers.push(...matchingPRs); - } - - return prNumbers; - - - name: Comment and close pull requests - if: ${{ steps.trac-tickets.outputs.fixed_list != '' && steps.git-svn-id.outputs.svn_revision_number != '' }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const prNumbers = ${{ steps.linked-prs.outputs.result }}; - - const commentBody = `A commit was made that fixes the Trac ticket referenced in the description of this pull request. - - SVN changeset: [${{ steps.git-svn-id.outputs.svn_revision_number }}](https://core.trac.wordpress.org/changeset/${{ steps.git-svn-id.outputs.svn_revision_number }}) - GitHub commit: https://github.com/WordPress/wordpress-develop/commit/${{ github.sha }} - - This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.`; - - // Update all matched pull requests. - for (const prNumber of prNumbers) { - // Comment on the pull request with details. - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: commentBody - }); - - // Close the pull request. - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber, - state: 'closed' - }); - } diff --git a/.github/workflows/reusable-coding-standards-javascript.yml b/.github/workflows/reusable-coding-standards-javascript.yml deleted file mode 100644 index 5c9a0c1ec0d03..0000000000000 --- a/.github/workflows/reusable-coding-standards-javascript.yml +++ /dev/null @@ -1,61 +0,0 @@ -## -# A reusable workflow that checks the JavaScript coding standards. -## -name: JavaScript coding standards - -on: - workflow_call: - -env: - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the JavaScript coding standards checks. - # - # JSHint violations are not currently reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Logs debug information about the GitHub Action runner. - # - Installs npm dependencies. - # - Run the WordPress JSHint checks. - # - Ensures version-controlled files are not modified or deleted. - jshint: - name: JavaScript checks - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Log debug information - run: | - npm --version - node --version - git --version - - - name: Install npm Dependencies - run: npm ci - - - name: Run JSHint - run: npm run grunt jshint - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-coding-standards-php.yml b/.github/workflows/reusable-coding-standards-php.yml deleted file mode 100644 index 1213ccb6baa6f..0000000000000 --- a/.github/workflows/reusable-coding-standards-php.yml +++ /dev/null @@ -1,108 +0,0 @@ -## -# A reusable workflow that checks the PHP coding standards. -## -name: PHP coding standards - -on: - workflow_call: - inputs: - php-version: - description: 'The PHP version to use.' - required: false - type: 'string' - default: 'latest' - old-branch: - description: 'Whether this is an old branch that runs phpcbf instead of phpcs' - required: false - type: 'boolean' - default: false - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the PHP coding standards checks. - # - # Violations are reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up PHP. - # - Configures caching for PHPCS scans. - # - Installs Composer dependencies. - # - Make Composer packages available globally. - # - Runs PHPCS on the full codebase (warnings excluded). - # - Generate a report for displaying issues as pull request annotations. - # - Runs PHPCS on the `tests` directory without (warnings included). - # - Generate a report for displaying `test` directory issues as pull request annotations. - # - Ensures version-controlled files are not modified or deleted. - phpcs: - name: PHP checks - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: ${{ inputs.php-version }} - coverage: none - tools: cs2pr - - # This date is used to ensure that the PHPCS cache is cleared at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" - - - name: Cache PHPCS scan cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: | - .cache/phpcs-src.json - .cache/phpcs-tests.json - key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-php-${{ inputs.php-version }}-phpcs-cache-${{ hashFiles('**/composer.json', 'phpcs.xml.dist') }} - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: ${{ steps.get-date.outputs.date }} - - - name: Make Composer packages available globally - run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" - - - name: Run PHPCS on all Core files - id: phpcs-core - if: ${{ ! inputs.old-branch }} - run: phpcs -n --report-full --cache=./.cache/phpcs-src.json --report-checkstyle=./.cache/phpcs-report.xml - - - name: Show PHPCS results in PR - if: ${{ always() && steps.phpcs-core.outcome == 'failure' }} - run: cs2pr ./.cache/phpcs-report.xml - - - name: Check test suite files for warnings - id: phpcs-tests - if: ${{ ! inputs.old-branch }} - run: phpcs tests --report-full --cache=./.cache/phpcs-tests.json --report-checkstyle=./.cache/phpcs-tests-report.xml - - - name: Show test suite scan results in PR - if: ${{ always() && steps.phpcs-tests.outcome == 'failure' }} - run: cs2pr ./.cache/phpcs-tests-report.xml - - - name: Run PHPCBF on all Core files (old branches) - if: ${{ inputs.old-branch }} - run: phpcbf - - - name: Ensure version-controlled files are not modified during the tests - run: git diff --exit-code diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml deleted file mode 100644 index 0b2ceec077602..0000000000000 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ /dev/null @@ -1,157 +0,0 @@ -## -# A reusable workflow that runs end-to-end tests. -# -# Branches 6.3 and earlier used Puppeteer instead of Playwright. -# Use https://github.com/WordPress/wordpress-develop/tree/6.3/.github/workflows/reusable-end-to-end-tests.yml instead. -## -name: End-to-end Tests - -on: - workflow_call: - inputs: - LOCAL_SCRIPT_DEBUG: - description: 'Whether to enable script debugging.' - required: false - type: 'boolean' - default: false - php-version: - description: 'The PHP version to use.' - required: false - type: 'string' - default: 'latest' - install-gutenberg: - description: 'Whether to install the Gutenberg plugin.' - required: false - type: 'boolean' - default: true - gutenberg-version: - description: 'A specific version of Gutenberg to install.' - required: false - type: 'string' - install-playwright: - description: 'Whether to install Playwright browsers.' - required: false - type: 'boolean' - default: true - -env: - LOCAL_DIR: build - LOCAL_PHP: ${{ inputs.php-version }}${{ 'latest' != inputs.php-version && '-fpm' || '' }} - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the end-to-end test suite. - # - # Performs the following steps: - # - Sets environment variables. - # - Checks out the repository. - # - Sets up Node.js. - # - Logs debug information about the GitHub Action runner. - # - Installs npm dependencies. - # - Install Playwright browsers. - # - Builds WordPress to run from the `build` directory. - # - Starts the WordPress Docker container. - # - Logs the running Docker containers. - # - Logs Docker debug information (about both the Docker installation within the runner and the WordPress container). - # - Install WordPress within the Docker container. - # - Install Gutenberg. - # - Install additional languages. - # - Run the E2E tests. - # - Uploads screenshots and HTML snapshots as an artifact. - # - Ensures version-controlled files are not modified or deleted. - e2e-tests: - name: SCRIPT_DEBUG ${{ inputs.LOCAL_SCRIPT_DEBUG && 'enabled' || 'disabled' }} - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 20 - - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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 - if: ${{ inputs.install-playwright }} - run: npx playwright install --with-deps chromium - - - name: Build WordPress - run: npm run build - - - name: Start Docker environment - run: | - npm run env:start - - - 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 - env: - LOCAL_SCRIPT_DEBUG: ${{ inputs.LOCAL_SCRIPT_DEBUG }} - run: npm run env:install - - - name: Install Gutenberg - if: ${{ inputs.install-gutenberg }} - run: | - npm run env:cli -- plugin install gutenberg \ - ${{ inputs.gutenberg-version && '--version="${GUTENBERG_VERSION}"' || '' }} \ - --path="/var/www/${LOCAL_DIR}" - env: - GUTENBERG_VERSION: ${{ inputs.gutenberg-version }} - - - 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}" - - - name: Run E2E tests - run: npm run test:e2e - - - name: Archive debug artifacts (screenshots, HTML snapshots) - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: always() - with: - name: failures-artifacts${{ inputs.LOCAL_SCRIPT_DEBUG && '-SCRIPT_DEBUG' || '' }}-${{ github.run_id }} - path: artifacts - if-no-files-found: ignore - include-hidden-files: true - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-javascript-tests.yml b/.github/workflows/reusable-javascript-tests.yml deleted file mode 100644 index 6bab6a5287665..0000000000000 --- a/.github/workflows/reusable-javascript-tests.yml +++ /dev/null @@ -1,71 +0,0 @@ -## -# A reusable workflow that runs JavaScript tests. -## -name: JavaScript tests - -on: - workflow_call: - inputs: - disable-apparmor: - description: 'Whether to disable AppArmor.' - required: false - type: 'boolean' - default: false - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the QUnit test suite. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Logs debug information about the GitHub Action runner. - # - Installs npm dependencies. - # - Run the WordPress QUnit tests. - # - Ensures version-controlled files are not modified or deleted. - test-js: - name: Run QUnit tests - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Log debug information - run: | - npm --version - node --version - git --version - - - name: Install npm Dependencies - run: npm ci - - # Older branches using outdated versions of Puppeteer fail on newer versions of the `ubuntu-24` image. - # This disables AppArmor in order to work around those failures. - # - # See https://issues.chromium.org/issues/373753919 - # and https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md - - name: Disable AppArmor - if: ${{ inputs.disable-apparmor }} - run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - - - name: Run QUnit tests - run: npm run grunt qunit:compiled - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-javascript-type-checking-v1.yml b/.github/workflows/reusable-javascript-type-checking-v1.yml deleted file mode 100644 index 7eab9346f2147..0000000000000 --- a/.github/workflows/reusable-javascript-type-checking-v1.yml +++ /dev/null @@ -1,76 +0,0 @@ -## -# A reusable workflow that runs JavaScript Type Checking. -## -name: JavaScript Type Checking - -on: - workflow_call: - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs JavaScript type checking. - # - # Violations are reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Logs debug information. - # - Installs npm dependencies. - # - Configures caching for TypeScript build info. - # - Runs JavaScript type checking. - # - Saves the TypeScript build info. - # - Ensures version-controlled files are not modified or deleted. - typecheck: - name: Run JavaScript type checking - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 10 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Log debug information - run: | - npm --version - node --version - - - name: Install npm dependencies - run: npm ci --ignore-scripts - - - name: Cache TypeScript build info - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: | - *.tsbuildinfo - key: "ts-build-info-${{ github.run_id }}" - restore-keys: | - ts-build-info- - - - name: Run JavaScript type checking - run: npm run typecheck:js - - - name: "Save result cache" - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - if: ${{ !cancelled() }} - with: - path: | - *.tsbuildinfo - key: "ts-build-info-${{ github.run_id }}" - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-performance-report-v2.yml b/.github/workflows/reusable-performance-report-v2.yml deleted file mode 100644 index 1b158bb6813ae..0000000000000 --- a/.github/workflows/reusable-performance-report-v2.yml +++ /dev/null @@ -1,114 +0,0 @@ -## -# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Download artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - 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: 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 deleted file mode 100644 index f572060e26d63..0000000000000 --- a/.github/workflows/reusable-performance-test-v2.yml +++ /dev/null @@ -1,267 +0,0 @@ -## -# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - 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: 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - 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 diff --git a/.github/workflows/reusable-performance.yml b/.github/workflows/reusable-performance.yml deleted file mode 100644 index 923b472f609c6..0000000000000 --- a/.github/workflows/reusable-performance.yml +++ /dev/null @@ -1,357 +0,0 @@ -## -# A reusable workflow that runs the performance test suite. -## -name: Run performance Tests - -on: - workflow_call: - inputs: - 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' - 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 - 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: "''" - - # This workflow takes two sets of measurements — one for the current commit, - # and another against a consistent version that is used as a baseline measurement. - # This is done to isolate variance in measurements caused by the GitHub runners - # from differences caused by code changes between commits. The BASE_TAG value here - # represents the version being used for baseline measurements. It should only be - # changed if we want to normalize results against a different baseline. - BASE_TAG: ${{ inputs.BASE_TAG }} - LOCAL_DIR: ${{ inputs.LOCAL_DIR }} - TARGET_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || '' }} - TARGET_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} - - 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. - # - Determine the target SHA value (on `workflow_dispatch` only). - # - Set up Node.js. - # - Log debug information. - # - Install npm dependencies. - # - Install Playwright browsers. - # - Build WordPress. - # - Start Docker environment. - # - Install object cache drop-in. - # - Log running Docker containers. - # - Docker debug information. - # - Install WordPress. - # - 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 (current commit). - # - Download previous build artifact (target branch or previous commit). - # - Download artifact. - # - Unzip the build. - # - Run any database upgrades. - # - Flush cache. - # - Delete expired transients. - # - Run performance tests (previous/target commit). - # - Set the environment to the baseline version. - # - Run any database upgrades. - # - Flush cache. - # - Delete expired transients. - # - Run baseline performance tests. - # - Archive artifacts. - # - Compare results. - # - Add workflow summary. - # - Set the base sha. - # - Set commit details. - # - Publish performance results. - # - Ensure version-controlled files are not modified or deleted. - performance: - name: ${{ inputs.multisite && 'Multisite' || 'Single site' }} / ${{ inputs.memcached && 'Memcached' || 'Default' }} - runs-on: ubuntu-24.04 - permissions: - contents: read - if: ${{ ! contains( github.event.before, '00000000' ) }} - - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - fetch-depth: ${{ github.event_name == 'workflow_dispatch' && '2' || '1' }} - persist-credentials: false - - # 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 up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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: Build WordPress - run: npm run build - - - name: Start Docker environment - run: npm run env:start - - - 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: 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: 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 (current commit) - run: npm run test:performance - - - name: Download previous build artifact (target branch or previous commit) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - 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 build - if: ${{ steps.get-previous-build.outputs.result }} - run: | - unzip "${GITHUB_WORKSPACE}/before.zip" - unzip -o "${GITHUB_WORKSPACE}/wordpress.zip" - - - name: Run any database upgrades - if: ${{ steps.get-previous-build.outputs.result }} - run: npm run env:cli -- core update-db --path="/var/www/${LOCAL_DIR}" - - - name: Flush cache - if: ${{ steps.get-previous-build.outputs.result }} - run: npm run env:cli -- cache flush --path="/var/www/${LOCAL_DIR}" - - - name: Delete expired transients - if: ${{ steps.get-previous-build.outputs.result }} - run: npm run env:cli -- transient delete --expired --path="/var/www/${LOCAL_DIR}" - - - name: Run target performance tests (previous/target commit) - if: ${{ steps.get-previous-build.outputs.result }} - env: - TEST_RESULTS_PREFIX: before - run: npm run test:performance - - - name: Set the environment to the baseline version - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} - run: | - VERSION="${BASE_TAG}" - VERSION="${VERSION%.0}" - npm run env:cli -- core update --version="$VERSION" --force --path="/var/www/${LOCAL_DIR}" - npm run env:cli -- core version --path="/var/www/${LOCAL_DIR}" - - - name: Run any database upgrades - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} - run: npm run env:cli -- core update-db --path="/var/www/${LOCAL_DIR}" - - - name: Flush cache - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} - run: npm run env:cli -- cache flush --path="/var/www/${LOCAL_DIR}" - - - name: Delete expired transients - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} - run: npm run env:cli -- transient delete --expired --path="/var/www/${LOCAL_DIR}" - - - name: Run baseline performance tests - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} - env: - TEST_RESULTS_PREFIX: base - run: npm run test:performance - - - name: Archive artifacts - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: always() - with: - name: performance-artifacts${{ inputs.multisite && '-multisite' || '' }}${{ inputs.memcached && '-memcached' || '' }}-${{ github.run_id }} - path: artifacts - if-no-files-found: ignore - include-hidden-files: true - - - 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: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! inputs.memcached && ! inputs.multisite }} - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - id: base-sha - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - 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 - # Only publish results on pushes to trunk. - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! inputs.memcached && ! inputs.multisite }} - env: - BASE_SHA: ${{ steps.base-sha.outputs.result }} - CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} - HOST_NAME: "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" - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-php-compatibility.yml b/.github/workflows/reusable-php-compatibility.yml deleted file mode 100644 index fee371fbdf7a0..0000000000000 --- a/.github/workflows/reusable-php-compatibility.yml +++ /dev/null @@ -1,90 +0,0 @@ -## -# A reusable workflow that runs PHP compatibility tests. -## -name: PHP Compatibility - -on: - workflow_call: - inputs: - php-version: - description: 'The PHP version to use.' - required: false - type: 'string' - default: 'latest' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs PHP compatibility tests. - # - # Violations are reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up PHP. - # - Logs debug information. - # - Configures caching for PHP compatibility scans. - # - Installs Composer dependencies. - # - Make Composer packages available globally. - # - Runs the PHP compatibility tests. - # - Generate a report for displaying issues as pull request annotations. - # - Ensures version-controlled files are not modified or deleted. - php-compatibility: - name: Run compatibility checks - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: ${{ inputs.php-version }} - coverage: none - tools: cs2pr - - - name: Log debug information - run: | - composer --version - - # This date is used to ensure that the PHP compatibility cache is cleared at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" - - - name: Cache PHP compatibility scan cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: .cache/phpcompat.json - key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-php-${{ inputs.php-version }}-phpcompat-cache-${{ hashFiles('**/composer.json', 'phpcompat.xml.dist') }} - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: ${{ steps.get-date.outputs.date }} - - - name: Make Composer packages available globally - run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" - - - name: Run PHP compatibility tests - id: phpcs - run: phpcs --standard=phpcompat.xml.dist --report-full --report-checkstyle=./.cache/phpcs-compat-report.xml - - - name: Show PHPCompatibility results in PR - if: ${{ always() && steps.phpcs.outcome == 'failure' }} - run: cs2pr ./.cache/phpcs-compat-report.xml - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml deleted file mode 100644 index bbf1b78589a8c..0000000000000 --- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml +++ /dev/null @@ -1,109 +0,0 @@ -## -# A reusable workflow that runs PHP Static Analysis tests. -## -name: PHP Static Analysis - -on: - workflow_call: - inputs: - php-version: - description: 'The PHP version to use.' - required: false - type: 'string' - default: 'latest' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs PHP static analysis tests. - # - # Violations are reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up PHP. - # - Logs debug information. - # - Installs Composer dependencies. - # - Configures caching for PHP static analysis scans. - # - Make Composer packages available globally. - # - Runs PHPStan static analysis (with Pull Request annotations). - # - Saves the PHPStan result cache. - # - Ensures version-controlled files are not modified or deleted. - phpstan: - name: Run PHP static analysis - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: ${{ inputs.php-version }} - coverage: none - tools: cs2pr - - # This date is used to ensure that the Composer cache is cleared at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" - - - name: General debug information - run: | - npm --version - node --version - composer --version - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: ${{ steps.get-date.outputs.date }} - - - name: Make Composer packages available globally - run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" - - - name: Install npm dependencies - run: npm ci --ignore-scripts - - - name: Build WordPress - run: npm run build:dev - - - name: Cache PHP Static Analysis scan cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: .cache # This is defined in the base.neon file. - key: "phpstan-result-cache-${{ github.run_id }}" - restore-keys: | - phpstan-result-cache- - - - name: Run PHP static analysis tests - id: phpstan - run: composer run phpstan -- -vvv --error-format=checkstyle | cs2pr --errors-as-warnings --graceful-warnings - - - name: "Save result cache" - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - if: ${{ !cancelled() }} - with: - path: .cache - key: "phpstan-result-cache-${{ github.run_id }}" - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-phpunit-tests-v1.yml b/.github/workflows/reusable-phpunit-tests-v1.yml deleted file mode 100644 index bcb0451d7134b..0000000000000 --- a/.github/workflows/reusable-phpunit-tests-v1.yml +++ /dev/null @@ -1,197 +0,0 @@ -## -# DEPRECATED -# -# A reusable workflow that runs the PHPUnit test suite with the specified configuration. -# -# This workflow is used by branches 4.7 through 5.1. -## -name: Run PHPUnit tests - -on: - workflow_call: - inputs: - os: - description: 'Operating system to run tests on' - required: false - type: 'string' - default: 'ubuntu-24.04' - php: - description: 'The version of PHP to use, in the format of X.Y' - required: true - type: 'string' - phpunit: - description: 'The PHPUnit version to use when running tests. See .env for details about valid values.' - required: false - type: 'string' - default: ${{ inputs.php }}-fpm - multisite: - description: 'Whether to run tests as multisite' - required: false - type: 'boolean' - default: false - split_slow: - description: 'Whether to run slow tests group.' - required: false - type: 'boolean' - default: false - memcached: - description: 'Whether to test with memcached enabled' - required: false - type: 'boolean' - default: false - phpunit-config: - description: 'The PHPUnit configuration file to use' - required: false - type: 'string' - default: 'phpunit.xml.dist' - allow-errors: - description: 'Whether to continue when test errors occur.' - required: false - type: boolean - default: false -env: - COMPOSER_INSTALL: ${{ false }} - LOCAL_PHP: ${{ inputs.php }}-fpm - LOCAL_PHPUNIT: ${{ inputs.phpunit && inputs.phpunit || inputs.php }}-fpm - LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} - PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} - PHPUNIT_SCRIPT: php - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - SLOW_TESTS: 'external-http,media' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the PHPUnit tests for WordPress. - # - # Performs the following steps: - # - Sets environment variables. - # - Sets up the environment variables needed for testing with memcached (if desired). - # - Installs NodeJS. - # - Build WordPress - # _ Installs npm dependencies. - # - Configures caching for Composer. - # _ Installs Composer dependencies (if desired). - # - Logs Docker debug information (about the Docker installation within the runner). - # - Starts the WordPress Docker container. - # - Starts the Memcached server after the Docker network has been created (if desired). - # - Logs general debug information about the runner. - # - Logs the running Docker containers. - # - Logs debug information from inside the WordPress Docker container. - # - Logs debug information about what's installed within the WordPress Docker containers. - # - Install WordPress within the Docker container. - # - Run the PHPUnit tests. - test-php: - name: PHP ${{ inputs.php }} / ${{ inputs.multisite && ' Multisite' || 'Single site' }}${{ inputs.split_slow && ' slow tests' || '' }}${{ inputs.memcached && ' with memcached' || '' }} - runs-on: ${{ inputs.os }} - timeout-minutes: 20 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Install Dependencies - run: npm ci - - - name: Build WordPress - run: npm run build - - - name: Get composer cache directory - if: ${{ env.COMPOSER_INSTALL == true }} - id: composer-cache - run: echo "composer_dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - - - name: Cache Composer dependencies - if: ${{ env.COMPOSER_INSTALL == true }} - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - env: - cache-name: cache-composer-dependencies - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} - - - name: Install Composer dependencies - if: ${{ env.COMPOSER_INSTALL == true }} - run: | - docker compose run --rm php composer --version - docker compose run --rm php composer install - - - name: Docker debug information - run: | - docker -v - docker compose -v - - - name: Start Docker environment - run: | - npm run env:start - - # The memcached server needs to start after the Docker network has been set up with `npm run env:start`. - - name: Start the Memcached server. - if: ${{ inputs.memcached }} - run: | - cp tests/phpunit/includes/object-cache.php build/wp-content/object-cache.php - BASE=$(basename "$PWD") - docker run --name memcached --net "${BASE}_wpdevnet" -d memcached - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - - - name: Log running Docker containers - run: docker ps -a - - - name: WordPress Docker container debug information - run: | - 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: Run slow PHPUnit tests - if: ${{ inputs.split_slow }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group "${SLOW_TESTS}" - - - name: Run PHPUnit tests for single site excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-required" - - - name: Run PHPUnit tests for Multisite excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-excluded,oembed-headers" - - - name: Run PHPUnit tests - if: ${{ inputs.php >= '7.0' }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" - - - name: Run AJAX tests - if: ${{ ! inputs.multisite && ! inputs.split_slow }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ajax - - - name: Run external HTTP tests - if: ${{ ! inputs.multisite && ! inputs.split_slow }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c phpunit.xml.dist --group external-http diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml deleted file mode 100644 index 4e7b6716ebef1..0000000000000 --- a/.github/workflows/reusable-phpunit-tests-v2.yml +++ /dev/null @@ -1,212 +0,0 @@ -## -# DEPRECATED -# -# A reusable workflow that runs the PHPUnit test suite with the specified configuration. -# -# This workflow is used by branches 5.2 through 5.8. -## -name: Run PHPUnit tests - -on: - workflow_call: - inputs: - os: - description: 'Operating system to run tests on' - required: false - type: 'string' - default: 'ubuntu-24.04' - php: - description: 'The version of PHP to use, in the format of X.Y' - required: true - type: 'string' - multisite: - description: 'Whether to run tests as multisite' - required: false - type: 'boolean' - default: false - split_slow: - description: 'Whether to run slow tests group.' - required: false - type: 'boolean' - default: false - test_ajax: - description: 'Whether to run AJAX tests.' - required: false - type: 'boolean' - default: true - memcached: - description: 'Whether to test with memcached enabled' - required: false - type: 'boolean' - default: false - phpunit-config: - description: 'The PHPUnit configuration file to use' - required: false - type: 'string' - default: 'phpunit.xml.dist' - report: - description: 'Whether to report results to WordPress.org Hosting Tests' - required: false - type: 'boolean' - default: false - allow-errors: - description: 'Whether to continue when test errors occur.' - required: false - type: boolean - default: false -env: - LOCAL_PHP: ${{ inputs.php }}-fpm - LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} - PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - # Controls which npm script to use for running PHPUnit tests. Options ar `php` and `php-composer`. - PHPUNIT_SCRIPT: php - SLOW_TESTS: 'external-http,media' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the PHPUnit tests for WordPress. - # - # Performs the following steps: - # - Sets environment variables. - # - Checks out the repository. - # - Installs Node.js. - # - Installs npm dependencies - # - Configures caching for Composer. - # - Installs Composer dependencies. - # - Logs Docker debug information (about the Docker installation within the runner). - # - Starts the WordPress Docker container. - # - Logs general debug information about the runner. - # - Logs the running Docker containers. - # - Logs debug information from inside the WordPress Docker container. - # - Install WordPress within the Docker container. - # - Run the PHPUnit tests. - # - Ensures version-controlled files are not modified or deleted. - test-php: - name: PHP ${{ inputs.php }} / ${{ inputs.multisite && ' Multisite' || 'Single Site' }}${{ inputs.split_slow && ' slow tests' || '' }}${{ inputs.memcached && ' with memcached' || '' }} - runs-on: ${{ inputs.os }} - timeout-minutes: 20 - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Install Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Install npm dependencies - run: npm ci - - - name: Get composer cache directory - id: composer-cache - run: echo "composer_dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - - - name: Cache Composer dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - env: - cache-name: cache-composer-dependencies - with: - path: ${{ steps.composer-cache.outputs.composer_dir }} - key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} - - - name: Install Composer dependencies - run: | - docker compose run --rm php composer --version - - # The PHPUnit 7.x phar is not compatible with PHP 8 and won't be updated, - # as PHPUnit 7 is no longer supported. The Composer-installed PHPUnit should be - # used for PHP 8 testing instead. - if [ "${LOCAL_PHP}" == '8.0-fpm' ]; then - docker compose run --rm php composer install --ignore-platform-reqs - echo "PHPUNIT_SCRIPT=php-composer" >> "$GITHUB_ENV" - elif [ "${LOCAL_PHP}" == '7.1-fpm' ]; then - docker compose run --rm php composer update - git checkout -- composer.lock - elif [[ "${LOCAL_PHP}" == '5.6-fpm' || "${LOCAL_PHP}" == '7.0-fpm' ]]; then - docker compose run --rm php composer require --dev phpunit/phpunit:"^5.7" --update-with-dependencies - git checkout -- composer.lock composer.json - else - docker compose run --rm php composer install - fi - - - name: Docker debug information - run: | - docker -v - docker compose -v - - - name: Start Docker environment - run: | - npm run env:start - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - - - name: Log running Docker containers - run: docker ps -a - - - name: WordPress Docker container debug information - run: | - 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: Run slow PHPUnit tests - if: ${{ inputs.split_slow }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group "${SLOW_TESTS}" - - - name: Run PHPUnit tests for single site excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-required" - - - name: Run PHPUnit tests for Multisite excluding slow tests - if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-excluded,oembed-headers" - - - name: Run PHPUnit tests - if: ${{ inputs.php >= '7.0' }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" - - - name: Run AJAX tests - if: ${{ ! inputs.split_slow&& inputs.test_ajax }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ajax - - - name: Run ms-files tests as a multisite install - if: ${{ inputs.multisite && ! inputs.split_slow }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ms-files - - - name: Run external HTTP tests - if: ${{ ! inputs.multisite && ! inputs.split_slow }} - run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c phpunit.xml.dist --group external-http - - # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. - - name: Run (xDebug) tests - if: ${{ ! inputs.split_slow }} - run: LOCAL_PHP_XDEBUG=true npm run "test:${PHPUNIT_SCRIPT}" -- -v --group xdebug --exclude-group __fakegroup__ - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml deleted file mode 100644 index da0372f8538be..0000000000000 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ /dev/null @@ -1,271 +0,0 @@ -## -# A reusable workflow that runs the PHPUnit test suite with the specified configuration. -# -# This workflow is used by `trunk` and branches >= 5.9. -## -name: Run PHPUnit tests - -on: - workflow_call: - inputs: - os: - description: 'Operating system to run tests on' - required: false - type: 'string' - default: 'ubuntu-24.04' - php: - description: 'The version of PHP to use, in the format of X.Y' - required: true - type: 'string' - db-type: - description: 'Database type. Valid types are mysql and mariadb' - required: false - type: 'string' - default: 'mysql' - db-version: - description: 'Database version' - required: false - type: 'string' - default: '8.4' - db-innovation: - description: 'Whether a database software innovation release is being tested.' - required: false - type: 'boolean' - default: false - multisite: - description: 'Whether to run tests as multisite' - required: false - type: 'boolean' - default: false - memcached: - description: 'Whether to test with memcached enabled' - required: false - type: 'boolean' - default: false - phpunit-config: - description: 'The PHPUnit configuration file to use' - required: false - type: 'string' - default: 'phpunit.xml.dist' - phpunit-test-groups: - description: 'A list of test groups to run.' - required: false - type: 'string' - default: '' - tests-domain: - description: 'The domain to use for the tests' - required: false - type: 'string' - default: 'example.org' - coverage-report: - description: 'Whether to generate a code coverage report.' - required: false - type: boolean - default: false - report: - description: 'Whether to report results to WordPress.org Hosting Tests' - required: false - type: 'boolean' - default: false - allow-errors: - description: 'Whether to continue when test errors occur.' - required: false - type: boolean - default: false - secrets: - CODECOV_TOKEN: - description: 'The Codecov token required for uploading reports.' - required: false - WPT_REPORT_API_KEY: - description: 'The WordPress.org Hosting Tests API key.' - required: false - -env: - LOCAL_PHP: ${{ inputs.php }}-fpm - LOCAL_PHP_XDEBUG: ${{ inputs.coverage-report || false }} - LOCAL_PHP_XDEBUG_MODE: ${{ inputs.coverage-report && 'coverage' || 'develop,debug' }} - LOCAL_DB_TYPE: ${{ inputs.db-type }} - LOCAL_DB_VERSION: ${{ inputs.db-version }} - LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} - LOCAL_WP_TESTS_DOMAIN: ${{ inputs.tests-domain }} - PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the PHPUnit tests for WordPress. - # - # Performs the following steps: - # - Sets environment variables. - # - Checks out the repository. - # - Sets up Node.js. - # - Sets up PHP. - # - Installs Composer dependencies. - # - Installs npm dependencies - # - Logs general debug information about the runner. - # - Logs Docker debug information (about the Docker installation within the runner). - # - Starts the WordPress Docker container. - # - Logs the running Docker containers. - # - Logs debug information about what's installed within the WordPress Docker containers. - # - Install WordPress within the Docker container. - # - Run the PHPUnit tests. - # - Upload the code coverage report to Codecov.io. - # - Upload the HTML code coverage report as an artifact. - # - Ensures version-controlled files are not modified or deleted. - # - Checks out the WordPress Test reporter repository. - # - Submit the test results to the WordPress.org host test results. - phpunit-tests: - name: ${{ ( inputs.phpunit-test-groups || inputs.coverage-report ) && format( 'PHP {0} with ', inputs.php ) || '' }} ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }}${{ inputs.db-innovation && ' (innovation release)' || '' }}${{ inputs.memcached && ' with memcached' || '' }}${{ inputs.report && ' (test reporting enabled)' || '' }} ${{ 'example.org' != inputs.tests-domain && inputs.tests-domain || '' }} - runs-on: ${{ inputs.os }} - timeout-minutes: ${{ inputs.coverage-report && 120 || inputs.php == '8.4' && 30 || 20 }} - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - ## - # This allows Composer dependencies to be installed using a single step. - # - # Since the tests are currently run within the Docker containers where the PHP version varies, - # the same PHP version needs to be configured for the action runner machine so that the correct - # dependency versions are installed and cached. - ## - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: '${{ inputs.php }}' - coverage: none - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F") - - - name: Install npm dependencies - run: npm ci - - - name: Build WordPress - run: npm run build:dev - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - composer --version - locale -a - - - name: Docker debug information - run: | - docker -v - - - name: Start Docker environment - run: | - npm run env:start - - - name: Log running Docker containers - run: docker ps -a - - - name: WordPress Docker container debug information - run: | - docker compose run --rm mysql "${LOCAL_DB_CMD}" --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 - env: - LOCAL_DB_CMD: ${{ env.LOCAL_DB_TYPE == 'mariadb' && contains( fromJSON('["5.5", "10.0", "10.1", "10.2", "10.3"]'), env.LOCAL_DB_VERSION ) && 'mysql' || env.LOCAL_DB_TYPE }} - - - name: Install WordPress - run: npm run env:install - - - name: Run PHPUnit tests${{ inputs.phpunit-test-groups && format( ' ({0} groups)', inputs.phpunit-test-groups ) || '' }}${{ inputs.coverage-report && ' with coverage report' || '' }} - continue-on-error: ${{ inputs.allow-errors }} - run: | - node ./tools/local-env/scripts/docker.js run \ - php ./vendor/bin/phpunit \ - --verbose \ - -c "${PHPUNIT_CONFIG}" \ - ${{ inputs.phpunit-test-groups && '--group "${TEST_GROUPS}"' || '' }} \ - ${{ inputs.coverage-report && '--coverage-clover "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}.xml" --coverage-html "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}"' || '' }} - env: - TEST_GROUPS: ${{ inputs.phpunit-test-groups }} - MULTISITE_FLAG: ${{ inputs.multisite && 'multisite' || 'single' }} - - - name: Run AJAX tests - if: ${{ ! inputs.phpunit-test-groups && ! inputs.coverage-report }} - continue-on-error: ${{ inputs.allow-errors }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group ajax - - - name: Run ms-files tests as a multisite install - if: ${{ inputs.multisite && ! inputs.phpunit-test-groups && ! inputs.coverage-report }} - continue-on-error: ${{ inputs.allow-errors }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group ms-files - - - name: Run external HTTP tests - if: ${{ ! inputs.multisite && ! inputs.phpunit-test-groups && ! inputs.coverage-report }} - continue-on-error: ${{ inputs.allow-errors }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group external-http - - # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. - - name: Run (Xdebug) tests - if: ${{ ! inputs.phpunit-test-groups && ! inputs.coverage-report }} - continue-on-error: ${{ inputs.allow-errors }} - run: LOCAL_PHP_XDEBUG=true node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit -v --group xdebug --exclude-group __fakegroup__ - - - name: Upload test coverage report to Codecov - if: ${{ inputs.coverage-report }} - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }}.xml - flags: ${{ inputs.multisite && 'multisite' || 'single' }},php - fail_ci_if_error: true - - - name: Upload HTML coverage report as artifact - if: ${{ inputs.coverage-report }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }} - path: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }} - overwrite: true - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code - - - name: Checkout the WordPress Test Reporter - if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: 'WordPress/phpunit-test-runner' - path: 'test-runner' - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Submit test results to the WordPress.org host test results - if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }} - env: - WPT_REPORT_API_KEY: "${{ secrets.WPT_REPORT_API_KEY }}" - run: docker compose run --rm -e WPT_REPORT_API_KEY -e WPT_PREPARE_DIR=/var/www -e WPT_TEST_DIR=/var/www php php test-runner/report.php diff --git a/.github/workflows/reusable-support-json-reader-v1.yml b/.github/workflows/reusable-support-json-reader-v1.yml deleted file mode 100644 index be5693aac7297..0000000000000 --- a/.github/workflows/reusable-support-json-reader-v1.yml +++ /dev/null @@ -1,155 +0,0 @@ -## -# A reusable workflow that reads the .version-support-*.json files and returns values for use in a -# test matrix based on a given WordPress version. -## -name: Determine test matrix values - -on: - workflow_call: - inputs: - wp-version: - description: 'The WordPress version to test . Accepts major and minor versions, "latest", or "nightly". Major releases must not end with ".0".' - type: string - default: 'nightly' - repository: - description: 'The repository to read support JSON files from.' - type: string - default: 'WordPress/wordpress-develop' - outputs: - major-wp-version: - description: "The major WordPress version based on the version provided in wp-version" - value: ${{ jobs.major-wp-version.outputs.version }} - php-versions: - description: "The PHP versions to test for the given wp-version" - value: ${{ jobs.php-versions.outputs.versions }} - mysql-versions: - description: "The MySQL versions to test for the given wp-version" - value: ${{ jobs.mysql-versions.outputs.versions }} - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Determines the major version of WordPress being tested. - # - # The data in the JSON files are indexed by major version, so this is used to look up the appropriate support policy. - # - # Performs the following steps: - # - Checks out the repository - # - Returns the major WordPress version as an output based on the value passed to the wp-version input. - major-wp-version: - name: Determine major WordPress version - permissions: - contents: read - runs-on: ubuntu-24.04 - timeout-minutes: 5 - outputs: - version: ${{ steps.major-wp-version.outputs.version }} - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: ${{ inputs.repository }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Determine the major WordPress version - id: major-wp-version - run: | - if [ "${WP_VERSION}" ] && [ "${WP_VERSION}" != "nightly" ] && [ "${WP_VERSION}" != "latest" ] && [ "${WP_VERSION}" != "trunk" ]; then - echo "version=$(echo "${WP_VERSION}" | tr '.' '-' | cut -d '-' -f1-2)" >> "$GITHUB_OUTPUT" - elif [ "${WP_VERSION}" ] && [ "${WP_VERSION}" != "trunk" ]; then - echo "version=${WP_VERSION}" >> "$GITHUB_OUTPUT" - else - echo "version=nightly" >> "$GITHUB_OUTPUT" - fi - env: - WP_VERSION: ${{ inputs.wp-version }} - - # Determines the versions of PHP supported for a version of WordPress. - # - # Performs the following steps: - # - Checks out the repository - # - Returns the versions of PHP supported for the major version of WordPress by parsing the - # .version-support-php.json file and returning the values in that version's index. - php-versions: - name: Determine PHP versions - permissions: - contents: read - runs-on: ubuntu-24.04 - needs: [ major-wp-version ] - timeout-minutes: 5 - outputs: - versions: ${{ steps.php-versions.outputs.versions }} - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: ${{ inputs.repository }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - # Look up the major version's specific PHP support policy when a version is provided. - # Otherwise, use the current PHP support policy. - - name: Get supported PHP versions - id: php-versions - run: | - if [ "${WP_VERSION}" != "latest" ] && [ "${WP_VERSION}" != "nightly" ]; then - VERSIONS="$( jq \ - -r \ - --arg wp_version "${WP_VERSION}" \ - '.[$wp_version] | @json' \ - .version-support-php.json - )" - echo "versions=$VERSIONS" >> "$GITHUB_OUTPUT" - else - echo "versions=$(jq -r '.[ (keys[-1]) ] | @json' .version-support-php.json)" >> "$GITHUB_OUTPUT" - fi - env: - WP_VERSION: ${{ needs.major-wp-version.outputs.version }} - - # Determines the versions of MySQL supported for a version of WordPress. - # - # Performs the following steps: - # - Checks out the repository - # - Returns the versions of MySQL supported for the major version of WordPress by parsing the - # .version-support-mysql.json file and returning the values in that version's index. - mysql-versions: - name: Determine MySQL versions - permissions: - contents: read - runs-on: ubuntu-24.04 - needs: [ major-wp-version ] - timeout-minutes: 5 - outputs: - versions: ${{ steps.mysql-versions.outputs.versions }} - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: ${{ inputs.repository }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - # Look up the major version's specific MySQL support policy when a version is provided. - # Otherwise, use the current MySQL support policy. - - name: Get supported MySQL versions - id: mysql-versions - run: | - if [ "${WP_VERSION}" != "latest" ] && [ "${WP_VERSION}" != "nightly" ]; then - VERSIONS="$( jq \ - -r \ - --arg wp_version "${WP_VERSION}" \ - '.[$wp_version] | @json' \ - .version-support-mysql.json - )" - echo "versions=$VERSIONS" >> "$GITHUB_OUTPUT" - else - echo "versions=$(jq -r '.[ (keys[-1]) ] | @json' .version-support-mysql.json)" >> "$GITHUB_OUTPUT" - fi - env: - WP_VERSION: ${{ needs.major-wp-version.outputs.version }} diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml deleted file mode 100644 index fbb6a08b15820..0000000000000 --- a/.github/workflows/reusable-test-core-build-process.yml +++ /dev/null @@ -1,158 +0,0 @@ -## -# A reusable workflow that tests the WordPress Core build process. -## -name: Test the WordPress Build Process - -on: - workflow_call: - inputs: - os: - description: 'Operating system to run tests on' - required: false - type: 'string' - default: 'ubuntu-24.04' - directory: - description: 'Directory to run WordPress from. Valid values are `src` or `build`' - required: false - type: 'string' - default: 'src' - test-emoji: - description: 'Whether to run the precommit:emoji Grunt script.' - required: false - type: 'boolean' - default: true - test-certificates: - description: 'Whether to run the certificate related Grunt scripts.' - required: false - type: 'boolean' - default: false - save-build: - description: 'Whether to save a ZIP of built WordPress as an artifact.' - required: false - type: 'boolean' - default: false - prepare-playground: - description: 'Whether to prepare the artifacts needed for Playground testing.' - required: false - type: 'boolean' - default: false - -env: - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - NODE_OPTIONS: --max-old-space-size=4096 - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Verifies that installing npm dependencies and building WordPress works as expected. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Logs debug information about the GitHub Action runner. - # - Installs npm dependencies. - # - Builds WordPress to run from the desired location (src or build). - # - Ensures version-controlled files are not modified or deleted. - # - Creates a ZIP of the built WordPress files (when building to the build directory). - # - Cleans up after building WordPress. - # - Ensures version-controlled files are not modified or deleted. - # - Uploads the ZIP as a GitHub Actions artifact (when building to the build directory). - # - Saves the pull request number to a text file. - # - Uploads the pull request number as an artifact. - build-process-tests: - name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }} - permissions: - contents: read - runs-on: ${{ inputs.os }} - timeout-minutes: 20 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - # This date is used to ensure that the PHPCS cache is cleared at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - if: ${{ inputs.test-certificates }} - run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - if: ${{ inputs.test-certificates }} - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: ${{ steps.get-date.outputs.date }} - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Log debug information - run: | - npm --version - node --version - curl --version - git --version - - - name: Install npm Dependencies - run: npm ci - - - name: Run Emoji precommit task - if: ${{ inputs.test-emoji }} - run: npm run grunt precommit:emoji - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Ensure certificates files are updated - if: ${{ inputs.test-certificates }} - run: npm run grunt copy:certificates && npm run grunt build:certificates - - - name: Build WordPress to run from ${{ inputs.directory }} - run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }} - - - name: Ensure version-controlled files are not modified or deleted during building - run: git diff --exit-code - - - name: Create ZIP of built files - if: ${{ inputs.directory == 'build' && contains( inputs.os, 'ubuntu-' ) }} - run: zip -r wordpress.zip build/. - - - name: Clean after building to run from ${{ inputs.directory }} - run: npm run grunt ${{ inputs.directory == 'src' && 'clean -- --dev' || 'clean' }} - - - name: Ensure version-controlled files are not modified or deleted during cleaning - run: git diff --exit-code - - - name: Upload ZIP as a GitHub Actions artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ inputs.save-build || inputs.prepare-playground }} - with: - name: wordpress-build-${{ github.event_name == 'pull_request' && github.event.number || github.sha }} - path: wordpress.zip - if-no-files-found: error - - - name: Save PR number - if: ${{ inputs.prepare-playground }} - run: | - mkdir -p ./pr-number - echo "${EVENT_NUMBER}" > ./pr-number/NR - env: - EVENT_NUMBER: ${{ github.event.number }} - - # Uploads the PR number as an artifact for the Pull Request Commenting workflow to download and then - # leave a comment detailing how to test the PR within WordPress Playground. - - name: Upload PR number as artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ inputs.prepare-playground && github.repository == 'WordPress/wordpress-develop' && github.event_name == 'pull_request' }} - with: - name: pr-number - path: pr-number/ diff --git a/.github/workflows/reusable-test-gutenberg-build-process.yml b/.github/workflows/reusable-test-gutenberg-build-process.yml deleted file mode 100644 index 6fff07a842bf2..0000000000000 --- a/.github/workflows/reusable-test-gutenberg-build-process.yml +++ /dev/null @@ -1,100 +0,0 @@ -## -# A reusable workflow that tests the Gutenberg plugin build process when run within a wordpress-develop checkout. -## -name: Test the Gutenberg plugin Build Process - -on: - workflow_call: - inputs: - os: - description: 'Operating system to run tests on' - required: false - type: 'string' - default: 'ubuntu-24.04' - directory: - description: 'Directory to run WordPress from. Valid values are `src` or `build`' - required: false - type: 'string' - default: 'src' - -env: - GUTENBERG_DIRECTORY: ${{ inputs.directory == 'build' && 'build' || 'src' }}/wp-content/plugins/gutenberg - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - NODE_OPTIONS: '--max-old-space-size=8192' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Verifies that installing npm dependencies and building the Gutenberg plugin works as expected. - # - # Performs the following steps: - # - Checks out the repository. - # - Checks out the Gutenberg plugin into the plugins directory. - # - Sets up Node.js. - # - Logs debug information about the GitHub Action runner. - # - Installs Gutenberg npm dependencies. - # - Runs the Gutenberg build process. - # - Installs Core npm dependencies. - # - Builds WordPress to run from the relevant location (src or build). - # - Builds Gutenberg. - # - Ensures version-controlled files are not modified or deleted. - build-process-tests: - name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }} - permissions: - contents: read - runs-on: ${{ inputs.os }} - timeout-minutes: 30 - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Checkout Gutenberg plugin - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: 'WordPress/gutenberg' - path: ${{ env.GUTENBERG_DIRECTORY }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - cache-dependency-path: | - package-lock.json - ${{ env.GUTENBERG_DIRECTORY }}/package-lock.json - - - name: Log debug information - run: | - npm --version - node --version - curl --version - git --version - - - name: Install Gutenberg Dependencies - run: npm ci - working-directory: ${{ env.GUTENBERG_DIRECTORY }} - - - name: Build Gutenberg - run: npm run build - working-directory: ${{ env.GUTENBERG_DIRECTORY }} - - - name: Install Core Dependencies - run: npm ci - - - name: Build WordPress to run from ${{ inputs.directory }} - run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }} - - - name: Run Gutenberg build script after building Core to run from ${{ inputs.directory }} - run: npm run build - working-directory: ${{ env.GUTENBERG_DIRECTORY }} - - - name: Ensure version-controlled files are not modified or deleted during building - run: git diff --exit-code diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml deleted file mode 100644 index 9aa0fb124a22e..0000000000000 --- a/.github/workflows/reusable-test-local-docker-environment-v1.yml +++ /dev/null @@ -1,170 +0,0 @@ -## -# A reusable workflow that ensures the local Docker environment is working properly. -# -# This workflow is used by `trunk` and branches >= 6.8. -## -name: Test local Docker environment - -on: - workflow_call: - inputs: - os: - description: 'Operating system to test' - required: false - type: 'string' - default: 'ubuntu-24.04' - php: - description: 'The version of PHP to use, in the format of X.Y' - required: false - type: 'string' - default: 'latest' - db-type: - description: 'Database type. Valid types are mysql and mariadb' - required: false - type: 'string' - default: 'mysql' - db-version: - description: 'Database version' - required: false - type: 'string' - default: '8.4' - memcached: - description: 'Whether to enable memcached' - required: false - type: 'boolean' - default: false - tests-domain: - description: 'The domain to use for the tests' - required: false - type: 'string' - default: 'example.org' - -env: - LOCAL_PHP: ${{ inputs.php == 'latest' && 'latest' || format( '{0}-fpm', inputs.php ) }} - LOCAL_DB_TYPE: ${{ inputs.db-type }} - LOCAL_DB_VERSION: ${{ inputs.db-version }} - LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} - LOCAL_WP_TESTS_DOMAIN: ${{ inputs.tests-domain }} - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Tests the local Docker environment. - # - # Performs the following steps: - # - Sets environment variables. - # - Checks out the repository. - # - Sets up Node.js. - # - Sets up PHP. - # - Installs Composer dependencies. - # - Installs npm dependencies - # - Logs general debug information about the runner. - # - Logs Docker debug information (about the Docker installation within the runner). - # - Starts the WordPress Docker container. - # - Logs the running Docker containers. - # - Logs debug information about what's installed within the WordPress Docker containers. - # - Install WordPress within the Docker container. - # - Restarts the Docker environment. - # - Runs a WP CLI command. - # - Tests the logs command. - # - Tests the reset command. - # - Ensures version-controlled files are not modified or deleted. - local-docker-environment-tests: - name: ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.memcached && ' with memcached' || '' }}${{ 'example.org' != inputs.tests-domain && format( ' {0}', inputs.tests-domain ) || '' }} - permissions: - contents: read - runs-on: ${{ inputs.os }} - timeout-minutes: 20 - - 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - - ## - # This allows Composer dependencies to be installed using a single step. - # - # Since tests are currently run within the Docker containers where the PHP version varies, - # the same PHP version needs to be configured for the action runner machine so that the correct - # dependency versions are installed and cached. - ## - - name: Set up PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: '${{ inputs.php }}' - coverage: none - - # Since Composer dependencies are installed using `composer update` and no lock file is in version control, - # passing a custom cache suffix ensures that the cache is flushed at least once per week. - - name: Install Composer dependencies - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4.0.0 - with: - custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F") - - - name: Install npm dependencies - run: npm ci - - - name: Build WordPress - run: npm run build:dev - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - composer --version - locale -a - - - name: Docker debug information - run: | - docker -v - - - name: Start Docker environment - run: | - npm run env:start - - - name: Log running Docker containers - run: docker ps -a - - - name: WordPress Docker container debug information - run: | - docker compose run --rm mysql "${LOCAL_DB_TYPE}" --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: Restart Docker environment - run: npm run env:restart - - - name: Test a CLI command - run: npm run env:cli option get siteurl - - - name: Test logs command - run: npm run env:logs - - - name: Reset the Docker environment - run: npm run env:reset - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml deleted file mode 100644 index 60d5523a9e3b6..0000000000000 --- a/.github/workflows/reusable-upgrade-testing.yml +++ /dev/null @@ -1,137 +0,0 @@ -# A reusable workflow that runs WordPress upgrade testing under the conditions provided. -name: Upgrade Tests - -on: - workflow_call: - inputs: - os: - description: 'Operating system to run tests on.' - required: false - type: 'string' - default: 'ubuntu-24.04' - wp: - description: 'The version of WordPress to start with.' - required: true - type: 'string' - new-version: - description: 'The version of WordPress to update to. Use "latest" to update to the latest version, "develop" to update to the current branch, or provide a specific version number to update to.' - type: 'string' - default: 'latest' - php: - description: 'The version of PHP to use. Expected format: X.Y.' - required: true - type: 'string' - multisite: - description: 'Whether to run tests as multisite.' - required: false - type: 'boolean' - default: false - db-type: - description: 'Database type. Valid types are mysql and mariadb.' - required: false - type: 'string' - default: 'mysql' - db-version: - description: 'Database version.' - required: false - type: 'string' - default: '5.7' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs upgrade tests on a build of WordPress. - # - # Performs the following steps: - # - Sets up PHP. - # - Downloads the specified version of WordPress. - # - Creates a `wp-config.php` file. - # - Installs WordPress. - # - Checks the version of WordPress before the upgrade. - # - Updates to the latest minor version. - # - Updates the database after the minor update. - # - Checks the version of WordPress after the minor update. - # - Updates to the version of WordPress being tested. - # - Updates the database. - # - Checks the version of WordPress after the upgrade. - upgrade-tests: - name: PHP ${{ inputs.php }} with ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }} - permissions: - contents: read - runs-on: ${{ inputs.os }} - timeout-minutes: 20 - - services: - database: - image: ${{ inputs.db-type }}:${{ inputs.db-version }} - ports: - - 3306 - options: >- - --health-cmd="mysqladmin ping" - --health-interval="30s" - --health-timeout="10s" - --health-retries="5" - -e MYSQL_ROOT_PASSWORD="root" - -e MYSQL_DATABASE="test_db" - - steps: - - name: Set up PHP ${{ inputs.php }} - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 - with: - php-version: '${{ inputs.php }}' - coverage: none - tools: wp-cli - - - name: Download WordPress ${{ inputs.wp }} - run: wp core download --version="${WP_VERSION}" - env: - WP_VERSION: ${{ inputs.wp }} - - - name: Create wp-config.php file - run: wp config create --dbname=test_db --dbuser=root --dbpass=root --dbhost="127.0.0.1:${DB_PORT}" - env: - DB_PORT: ${{ job.services.database.ports['3306'] }} - - - name: Install WordPress - run: | - wp core ${{ inputs.multisite && 'multisite-install' || 'install' }} \ - --url=http://localhost/ --title="Upgrade Test" --admin_user=admin \ - --admin_password=password --admin_email=me@example.org --skip-email - - - name: Pre-upgrade version check - run: wp core version - - - name: Update to the latest minor version - run: wp core update --minor - - - name: Update the database after the minor update - run: wp core update-db ${{ inputs.multisite && '--network' || '' }} - - - name: Post-upgrade version check after the minor update - run: wp core version - - - name: Download build artifact for the current branch - if: ${{ inputs.new-version == 'develop' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: wordpress-develop - - - name: Upgrade to WordPress at current branch - if: ${{ inputs.new-version == 'develop' }} - run: | - wp core update develop.zip - - - name: Upgrade to WordPress ${{ inputs.new-version }} - if: ${{ inputs.new-version != 'develop' }} - run: | - wp core update ${{ 'latest' != inputs.new-version && '--version="${WP_VERSION}"' || '' }} - env: - WP_VERSION: ${{ inputs.new-version }} - - - name: Update the database - run: wp core update-db ${{ inputs.multisite && '--network' || '' }} - - - name: Post-upgrade version check - run: wp core version diff --git a/.github/workflows/reusable-workflow-lint.yml b/.github/workflows/reusable-workflow-lint.yml deleted file mode 100644 index 3a538a8a99690..0000000000000 --- a/.github/workflows/reusable-workflow-lint.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Lint GitHub Actions workflows -on: - workflow_call: - -permissions: {} - -jobs: - # Runs the actionlint GitHub Action workflow file linter. - # - # This helps guard against common mistakes including strong type checking for expressions (${{ }}), security checks, - # `run:` script checking, glob syntax validation, and more. - # - # Performs the following steps: - # - Checks out the repository. - # - Runs actionlint. - actionlint: - name: Run actionlint - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 5 - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - # actionlint is static checker for GitHub Actions workflow files. - # See https://github.com/rhysd/actionlint. - - name: Run actionlint - uses: docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 # v1.7.7 - with: - args: "-color -verbose" diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml deleted file mode 100644 index 3d0dd7c680558..0000000000000 --- a/.github/workflows/slack-notifications.yml +++ /dev/null @@ -1,229 +0,0 @@ -## -# A reusable workflow for posting messages to the Making WordPress -# Core Slack Instance by submitting data to Slack webhook URLs -# received by Slack Workflows. -## -name: Slack Notifications - -on: - workflow_call: - inputs: - calling_status: - description: 'The status of the calling workflow' - type: string - required: true - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: - description: 'The Slack webhook URL for a successful build.' - required: true - SLACK_GHA_CANCELLED_WEBHOOK: - description: 'The Slack webhook URL for a cancelled build.' - required: true - SLACK_GHA_FIXED_WEBHOOK: - description: 'The Slack webhook URL for a fixed build.' - required: true - SLACK_GHA_FAILURE_WEBHOOK: - description: 'The Slack webhook URL for a failed build.' - required: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -env: - CURRENT_BRANCH: ${{ github.ref_name }} - -jobs: - # Gathers the details needed for Slack notifications. - # - # These details are passed as outputs to the subsequent, dependant jobs that - # submit data to Slack webhook URLs configured to post messages. - # - # Performs the following steps: - # - Retrieves the current workflow run. - # - Determines the conclusion of the previous workflow run or run attempt. - # - Sets the previous conclusion as an output. - # - Prepares the commit message. - # - Constructs and stores a message payload as an output. - prepare: - name: Prepare notifications - runs-on: ubuntu-24.04 - permissions: - actions: read - contents: read - timeout-minutes: 5 - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event.workflow_run.event != 'pull_request' }} - outputs: - previous_conclusion: ${{ steps.previous-attempt-result.outputs.result }} - payload: ${{ steps.create-payload.outputs.payload }} - - steps: - - name: Determine the status of the previous attempt - id: previous-attempt-result - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - retries: 2 - retry-exempt-status-codes: 418 - result-encoding: string - script: | - const workflow_run = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: `${context.runId}`, - }); - - if ( process.env.CALLING_STATUS == 'failure' && workflow_run.data.run_attempt == 1 ) { - return 'first-failure'; - } - - // When a workflow has been restarted, check the previous run attempt. Because workflows are automatically - // restarted once and a failure on the first run is not reported, failures on the second run should not be - // considered. - if ( workflow_run.data.run_attempt > 2 ) { - const previous_run = await github.rest.actions.getWorkflowRunAttempt({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: `${context.runId}`, - attempt_number: workflow_run.data.run_attempt - 1 - }); - - return previous_run.data.conclusion; - } - - // Otherwise, check the previous workflow run. - const previous_runs = await github.rest.actions.listWorkflowRuns({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: workflow_run.data.workflow_id, - branch: process.env.CURRENT_BRANCH, - exclude_pull_requests: true, - }); - - // This is the first workflow run for this branch or tag. - if ( previous_runs.data.workflow_runs.length < 2 ) { - return 'none'; - } - - const expected_events = new Array( 'push', 'schedule', 'workflow_dispatch' ); - - // Find the workflow run for the commit that immediately preceded this one. - for ( let i = 0; i < previous_runs.data.workflow_runs.length; i++ ) { - if ( previous_runs.data.workflow_runs[ i ].run_number == workflow_run.data.run_number ) { - let next_index = i; - do { - next_index++; - - // Protects against a false notification when contributors use the trunk branch as the pull request head_ref. - if ( expected_events.indexOf( previous_runs.data.workflow_runs[ next_index ].event ) == -1 ) { - continue; - } - - return previous_runs.data.workflow_runs[ next_index ].conclusion; - } while ( next_index < previous_runs.data.workflow_runs.length ); - } - } - - // Can't determine previous workflow conclusion. - return 'unknown'; - env: - CALLING_STATUS: ${{ inputs.calling_status }} - - - name: Get the commit message - id: current-commit-message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }} - with: - retries: 2 - retry-exempt-status-codes: 418 - result-encoding: string - script: | - const commit_details = await github.rest.repos.getCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: context.sha, - }); - return commit_details.data.commit.message; - - - name: Construct payload and store as an output - id: create-payload - run: | - COMMIT_MSG="$(echo "${COMMIT_MSG_RAW}" | awk 'NR==1')" - PAYLOAD="$( jq \ - -n \ - --arg workflow_name "${GITHUB_WORKFLOW}" \ - --arg ref_name "${CURRENT_BRANCH}" \ - --arg run_url "https://github.com/WordPress/wordpress-develop/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}" \ - --arg commit_message "${COMMIT_MSG}" \ - '{workflow_name: $workflow_name, ref_name: $ref_name, run_url: $run_url, commit_message: $commit_message}' | jq -c . - )" - echo "payload=$PAYLOAD" >> "$GITHUB_OUTPUT" - env: - COMMIT_MSG_RAW: ${{ ( github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' ) && steps.current-commit-message.outputs.result || github.event.head_commit.message }} - - # Posts notifications when a workflow fails. - failure: - name: Failure notifications - permissions: {} - runs-on: ubuntu-24.04 - timeout-minutes: 20 - needs: [ prepare ] - if: ${{ needs.prepare.outputs.previous_conclusion != 'first-failure' && inputs.calling_status == 'failure' || failure() }} - - steps: - - name: Post failure notifications to Slack - uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 - with: - webhook-type: webhook-trigger - webhook: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - payload: ${{ needs.prepare.outputs.payload }} - - # Posts notifications the first time a workflow run succeeds after previously failing. - fixed: - name: Fixed notifications - permissions: {} - runs-on: ubuntu-24.04 - timeout-minutes: 20 - needs: [ prepare ] - if: ${{ contains( fromJson( '["failure", "cancelled", "none"]' ), needs.prepare.outputs.previous_conclusion ) && inputs.calling_status == 'success' && success() }} - - steps: - - name: Post failure notifications to Slack - uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 - with: - webhook-type: webhook-trigger - webhook: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - payload: ${{ needs.prepare.outputs.payload }} - - # Posts notifications when a workflow is successful. - success: - name: Success notifications - permissions: {} - runs-on: ubuntu-24.04 - timeout-minutes: 20 - needs: [ prepare ] - if: ${{ inputs.calling_status == 'success' && success() }} - - steps: - - name: Post success notifications to Slack - uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 - with: - webhook-type: webhook-trigger - webhook: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - payload: ${{ needs.prepare.outputs.payload }} - - # Posts notifications when a workflow is cancelled. - cancelled: - name: Cancelled notifications - permissions: {} - runs-on: ubuntu-24.04 - timeout-minutes: 20 - needs: [ prepare ] - if: ${{ inputs.calling_status == 'cancelled' || cancelled() }} - - steps: - - name: Post cancelled notifications to Slack - uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 - with: - webhook-type: webhook-trigger - webhook: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - payload: ${{ needs.prepare.outputs.payload }} diff --git a/.github/workflows/test-and-zip-default-themes.yml b/.github/workflows/test-and-zip-default-themes.yml deleted file mode 100644 index 6ea0f7f206809..0000000000000 --- a/.github/workflows/test-and-zip-default-themes.yml +++ /dev/null @@ -1,305 +0,0 @@ -name: Test Default Themes & Create ZIPs - -on: - push: - branches: - - trunk - - '3.[89]' - - '[4-9].[0-9]' - paths: - # Changing the preferred version of Node.js could affect themes with build processes. - - '.npmrc' - - '.nvmrc' - # Changes to any themes with a build script should be confirmed. - - 'src/wp-content/themes/twentynineteen/**' - - 'src/wp-content/themes/twentytwenty/**' - - 'src/wp-content/themes/twentytwentyone/**' - - 'src/wp-content/themes/twentytwentytwo/**' - - 'src/wp-content/themes/twentytwentyfive/**' - # Changes to this workflow file should always verify success. - - '.github/workflows/test-and-zip-default-themes.yml' - pull_request: - branches: - - trunk - - '3.[89]' - - '[4-9].[0-9]' - paths: - # Changing the preferred version of Node.js could affect themes with build processes. - - '.npmrc' - - '.nvmrc' - # Changes to any themes with a build script should be confirmed. - - 'src/wp-content/themes/twentynineteen/**' - - 'src/wp-content/themes/twentytwenty/**' - - 'src/wp-content/themes/twentytwentyone/**' - - 'src/wp-content/themes/twentytwentytwo/**' - - 'src/wp-content/themes/twentytwentyfive/**' - # Changes to this workflow file should always verify success. - - '.github/workflows/test-and-zip-default-themes.yml' - workflow_dispatch: - inputs: - branch: - description: 'The branch to create ZIP files from' - required: true - type: string - default: 'trunk' - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Checks for zero-byte files. - # - # Occasionally, binary files such as images and fonts are added to themes incorrectly. - # This checks that all files contain contents. - # - # Performs the following steps: - # - Checks out the repository. - # - Checks for zero-byte (empty) files. - check-for-empty-files: - name: ${{ matrix.theme }} empty file check - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 10 - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - theme: [ - 'twentytwentyfive', - 'twentytwentyfour', - 'twentytwentythree', - 'twentytwentytwo', - 'twentytwentyone', - 'twentytwenty', - 'twentynineteen', - 'twentyseventeen', - 'twentysixteen', - 'twentyfifteen', - 'twentyfourteen', - 'twentythirteen', - 'twentytwelve', - 'twentyeleven', - 'twentyten' - ] - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Check for zero-byte (empty) files - run: | - [[ ! $(find "src/wp-content/themes/${THEME}" -empty) ]] - env: - THEME: ${{ matrix.theme }} - - # Tests the build script for themes that have one. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up Node.js. - # - Installs npm dependencies. - # - Runs the theme build script. - # - Ensures version-controlled files are not modified or deleted. - test-build-scripts: - name: Test ${{ matrix.theme }} build script - runs-on: ubuntu-24.04 - permissions: - contents: read - timeout-minutes: 10 - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - theme: [ - 'twentytwentyfive', - 'twentytwentytwo', - 'twentytwentyone', - 'twentytwenty', - 'twentynineteen', - ] - - defaults: - run: - working-directory: src/wp-content/themes/${{ matrix.theme }} - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - cache-dependency-path: src/wp-content/themes/${{ matrix.theme }}/package-lock.json - - - name: Install npm dependencies - run: npm ci - - - name: Build theme - run: npm run build - - - name: Check for changes to versioned files - id: built-file-check - if: ${{ github.event_name == 'pull_request' }} - run: | - if git diff --quiet; then - echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT" - else - echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT" - fi - - - name: Display changes to versioned files - if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} - run: git diff - - - name: Save diff to a file - if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} - run: git diff > ./changes.diff - - # Uploads the diff file as an artifact. - - name: Upload diff file as artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} - with: - name: pr-built-file-changes - path: src/wp-content/themes/${{ matrix.theme }}/changes.diff - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code - - # Prepares bundled themes for release. - # - # Performs the following steps: - # - Checks out the repository. - # - Uploads the theme files as a workflow artifact (files uploaded as an artifact are automatically zipped). - bundle-theme: - name: Create ${{ matrix.theme }} ZIP file - runs-on: ubuntu-24.04 - permissions: - contents: read - needs: [ check-for-empty-files, test-build-scripts ] - timeout-minutes: 10 - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - strategy: - fail-fast: false - matrix: - theme: [ - 'twentytwentyfive', - 'twentytwentyfour', - 'twentytwentythree', - 'twentytwentytwo', - 'twentytwentyone', - 'twentytwenty', - 'twentynineteen', - 'twentyseventeen', - 'twentysixteen', - 'twentyfifteen', - 'twentyfourteen', - 'twentythirteen', - 'twentytwelve', - 'twentyeleven', - 'twentyten' - ] - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Set up Node.js for themes needing minification - if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version-file: '.nvmrc' - cache: npm - cache-dependency-path: src/wp-content/themes/${{ matrix.theme }}/package-lock.json - - - name: Install npm dependencies - if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' - run: npm ci - working-directory: src/wp-content/themes/${{ matrix.theme }} - - - name: Build theme assets - if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' - run: npm run build - working-directory: src/wp-content/themes/${{ matrix.theme }} - - - name: Upload theme ZIP as an artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: ${{ matrix.theme }} - path: | - src/wp-content/themes/${{ matrix.theme }} - !src/wp-content/themes/${{ matrix.theme }}/node_modules - if-no-files-found: error - include-hidden-files: true - - slack-notifications: - name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml - permissions: - actions: read - contents: read - needs: [ check-for-empty-files, bundle-theme, test-build-scripts ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-24.04 - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: `${context.runId}`, - } - }); diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml index 184f85a323993..96d9d6abec0ab 100644 --- a/.github/workflows/test-build-processes.yml +++ b/.github/workflows/test-build-processes.yml @@ -51,7 +51,7 @@ jobs: # Tests the WordPress Core build process. test-core-build-process: name: Core running from ${{ matrix.directory }} - uses: ./.github/workflows/reusable-test-core-build-process.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-test-core-build-process.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} @@ -86,7 +86,7 @@ jobs: # See https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability. test-core-build-process-additional-os: name: Core running from ${{ matrix.directory }} - uses: ./.github/workflows/reusable-test-core-build-process.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-test-core-build-process.yml@trunk permissions: contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' }} @@ -101,7 +101,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml deleted file mode 100644 index deb190eba9e9b..0000000000000 --- a/.github/workflows/test-coverage.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Code Coverage Report - -on: - # Verify - push: - branches: - - trunk - paths: - - '.github/workflows/test-coverage.yml' - - '.github/workflows/reusable-phpunit-tests-v3.yml' - - 'docker-compose.yml' - - 'phpunit.xml.dist' - - 'tests/phpunit/multisite.xml' - pull_request: - branches: - - trunk - paths: - - '.github/workflows/test-coverage.yml' - - '.github/workflows/reusable-phpunit-tests-v3.yml' - - 'docker-compose.yml' - - 'phpunit.xml.dist' - - 'tests/phpunit/multisite.xml' - # Once daily at 00:00 UTC. - schedule: - - cron: '0 0 * * *' - # Allow manually triggering the workflow. - workflow_dispatch: - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -env: - LOCAL_PHP_XDEBUG: true - LOCAL_PHP_XDEBUG_MODE: 'coverage' - LOCAL_PHP_MEMCACHED: ${{ false }} - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - -jobs: - # - # Creates a PHPUnit test jobs for generating code coverage reports. - # - test-coverage-report: - name: ${{ matrix.multisite && 'Multisite' || 'Single site' }} report - uses: ./.github/workflows/reusable-phpunit-tests-v3.yml - permissions: - contents: read - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - strategy: - fail-fast: false - matrix: - multisite: [ false, true ] - coverage-report: [ true ] - with: - php: '8.3' - multisite: ${{ matrix.multisite }} - coverage-report: ${{ matrix.coverage-report }} - secrets: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - slack-notifications: - name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml - permissions: - actions: read - contents: read - needs: [ test-coverage-report ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-24.04 - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: `${context.runId}`, - } - }); diff --git a/.github/workflows/test-old-branches.yml b/.github/workflows/test-old-branches.yml deleted file mode 100644 index c3c7d2fee00fe..0000000000000 --- a/.github/workflows/test-old-branches.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Test old branches - -on: - # Verify the workflow is successful when this file is updated. - push: - branches: - - trunk - paths: - - '.github/workflows/test-old-branches.yml' - - '.github/workflows/reusable-phpunit-tests-v[1-2].yml' - # Run twice a month on the 1st and 15th at 00:00 UTC. - schedule: - - cron: '0 0 1 * *' - - cron: '0 0 15 * *' - workflow_dispatch: - inputs: - strategy: - description: 'The branches to test. Accepts X.Y branch names, or "all". Defaults to only the currently supported branch.' - required: false - type: string - default: '' - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -env: - CURRENTLY_SUPPORTED_BRANCH: '6.9' - -jobs: - dispatch-workflows-for-old-branches: - name: ${{ matrix.workflow }} for ${{ matrix.branch }} - runs-on: ubuntu-24.04 - permissions: - actions: write - timeout-minutes: 20 - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - strategy: - fail-fast: false - matrix: - workflow: [ - 'coding-standards.yml', - 'javascript-tests.yml', - 'phpunit-tests.yml', - 'test-build-processes.yml' - ] - branch: [ - '6.9', '6.8', '6.7', '6.6', '6.5', '6.4', '6.3', '6.2', '6.1','6.0', - '5.9', '5.8', '5.7', '5.6', '5.5', '5.4', '5.3', '5.2', '5.1', '5.0', - '4.9', '4.8', '4.7' - ] - include: - # PHP Compatibility testing was introduced in 5.5. - - branch: '6.9' - workflow: 'php-compatibility.yml' - - branch: '6.8' - workflow: 'php-compatibility.yml' - - branch: '6.7' - workflow: 'php-compatibility.yml' - - branch: '6.6' - workflow: 'php-compatibility.yml' - - branch: '6.5' - workflow: 'php-compatibility.yml' - - branch: '6.4' - workflow: 'php-compatibility.yml' - - branch: '6.3' - workflow: 'php-compatibility.yml' - - branch: '6.2' - workflow: 'php-compatibility.yml' - - branch: '6.1' - workflow: 'php-compatibility.yml' - - branch: '6.0' - workflow: 'php-compatibility.yml' - - branch: '5.9' - workflow: 'php-compatibility.yml' - - branch: '5.8' - workflow: 'php-compatibility.yml' - - branch: '5.7' - workflow: 'php-compatibility.yml' - - branch: '5.6' - workflow: 'php-compatibility.yml' - - branch: '5.5' - workflow: 'php-compatibility.yml' - - # End-to-end testing was introduced in 5.3 but was later removed as there were no meaningful assertions. - # Starting in 5.8 with #52905, some additional tests with real assertions were introduced. - # Branches 5.8 and newer should be tested to confirm no regressions are introduced. - - branch: '6.9' - workflow: 'end-to-end-tests.yml' - - branch: '6.8' - workflow: 'end-to-end-tests.yml' - - branch: '6.7' - workflow: 'end-to-end-tests.yml' - - branch: '6.6' - workflow: 'end-to-end-tests.yml' - - branch: '6.5' - workflow: 'end-to-end-tests.yml' - - branch: '6.4' - workflow: 'end-to-end-tests.yml' - - branch: '6.3' - workflow: 'end-to-end-tests.yml' - - branch: '6.2' - workflow: 'end-to-end-tests.yml' - - branch: '6.1' - workflow: 'end-to-end-tests.yml' - - branch: '6.0' - workflow: 'end-to-end-tests.yml' - - branch: '5.9' - workflow: 'end-to-end-tests.yml' - - branch: '5.8' - workflow: 'end-to-end-tests.yml' - - # Performance testing was introduced in 6.2 using Puppeteer but was overhauled to use Playwright instead in 6.4. - # Since the workflow frequently failed for 6.2 and 6.3 due to the flaky nature of the Puppeteer tests, - # the workflow was removed from those two branches. - - branch: '6.9' - workflow: 'performance.yml' - - branch: '6.8' - workflow: 'performance.yml' - - # Run all branches monthly, but only the currently supported one twice per month. - steps: - - name: Dispatch workflow run - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - if: ${{ github.event_name == 'push' || ( github.event_name == 'workflow_dispatch' && matrix.branch == inputs.strategy || inputs.strategy == 'all' ) || github.event.schedule == '0 0 15 * *' || matrix.branch == env.CURRENTLY_SUPPORTED_BRANCH }} - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: '${{ matrix.workflow }}', - ref: '${{ matrix.branch }}' - }); - - slack-notifications: - name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml - permissions: - actions: read - contents: read - needs: [ dispatch-workflows-for-old-branches ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} diff --git a/.github/workflows/upgrade-develop-testing.yml b/.github/workflows/upgrade-develop-testing.yml index 2b00536adeb66..c482dae51c845 100644 --- a/.github/workflows/upgrade-develop-testing.yml +++ b/.github/workflows/upgrade-develop-testing.yml @@ -46,7 +46,7 @@ jobs: # Build WordPress from the current branch ready for the upgrade tests. build: name: Build - uses: ./.github/workflows/reusable-build-package.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-build-package.yml@trunk if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} permissions: contents: read @@ -62,7 +62,7 @@ jobs: # being updated to keep the matrix as small as is reasonable. upgrade-tests-develop: name: Upgrade from ${{ matrix.wp }} - uses: ./.github/workflows/reusable-upgrade-testing.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-upgrade-testing.yml@trunk if: ${{ github.repository == 'WordPress/wordpress-develop' }} needs: [ build ] permissions: @@ -89,7 +89,7 @@ jobs: # Run a limited set of upgrade tests for the current branch on forks. upgrade-tests-develop-forks: name: Upgrade from ${{ matrix.wp }} - uses: ./.github/workflows/reusable-upgrade-testing.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-upgrade-testing.yml@trunk if: ${{ github.repository != 'WordPress/wordpress-develop' }} needs: [ build ] permissions: @@ -114,7 +114,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/.github/workflows/upgrade-testing.yml b/.github/workflows/upgrade-testing.yml deleted file mode 100644 index f042131bd7c26..0000000000000 --- a/.github/workflows/upgrade-testing.yml +++ /dev/null @@ -1,241 +0,0 @@ -# Confirms that updating WordPress using WP-CLI works successfully. -# -# This workflow is not meant to test wordpress-develop checkouts, but rather tagged versions officially available on WordPress.org. -name: Upgrade Tests - -on: - push: - branches: - - trunk - # Always test the workflow after it's updated. - paths: - - '.github/workflows/upgrade-testing.yml' - - '.github/workflows/reusable-upgrade-testing.yml' - pull_request: - # This workflow is only meant to run from trunk. Pull requests changing this file with different BASE branches should be ignored. - branches: - - trunk - # Always test the workflow when changes are suggested. - paths: - - '.github/workflows/upgrade-testing.yml' - - '.github/workflows/reusable-upgrade-testing.yml' - workflow_dispatch: - inputs: - new-version: - description: 'The version to test installing. Accepts major and minor versions, "latest", or "nightly". Major releases must not end with ".0".' - type: string - default: 'latest' - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ inputs.new-version || github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -# Because the number of jobs spawned can quickly balloon out of control, the following methodology is applied when -# building out the matrix below: -# -# - The two most recent releases of WordPress are tested against all PHP/MySQL LTS version combinations and the -# most recent innovation release. -# - The next 6 oldest versions of WordPress are tested against both the oldest and newest releases of PHP currently -# supported for both PHP 7 & 8 along with the oldest and newest MySQL LTS versions currently supported (no innovation -# releases). At the current 3 releases per year pace, this accounts for 2 additional years worth of releases. -# - Of the remaining versions of WordPress still receiving security updates, only test the ones where the database -# version was updated since the previous major release. -# - The oldest version of WordPress receiving security updates should always be tested against the same combinations as -# detailed for the two most recent releases. - -# Notes about chosen MySQL versions: -# - Only the most recent innovation release should be included in testing. -# - Even though MySQL >= 5.5.5 is currently supported, there are no 5.5.x Docker containers available that work on -# modern architectures. -# - 5.6.x Docker containers are available and work, but 5.6 only accounts for ~2.3% of installs as of 12/6/2024.defaults: -# - 5.7.x accounts for ~20% of installs, so this is used below instead. -jobs: - # Tests the full list of PHP/MySQL combinations for the two most recent versions of WordPress. - upgrade-tests-recent-releases: - name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} - uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ 'ubuntu-24.04' ] - php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] - db-type: [ 'mysql' ] - db-version: [ '5.7', '8.0', '8.4', '9.6' ] - wp: [ '6.8', '6.9' ] - multisite: [ false, true ] - with: - os: ${{ matrix.os }} - php: ${{ matrix.php }} - db-type: ${{ matrix.db-type }} - db-version: ${{ matrix.db-version }} - wp: ${{ matrix.wp }} - new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} - multisite: ${{ matrix.multisite }} - - # Tests 6.x releases where the WordPress database version changed on the oldest and newest supported versions of PHP 7 & 8. - upgrade-tests-wp-6x-mysql: - name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} - uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ 'ubuntu-24.04' ] - php: [ '7.4', '8.0', '8.4' ] - db-type: [ 'mysql' ] - db-version: [ '5.7', '8.4' ] - wp: [ '6.0', '6.3', '6.4', '6.5' ] - multisite: [ false, true ] - with: - os: ${{ matrix.os }} - php: ${{ matrix.php }} - db-type: ${{ matrix.db-type }} - db-version: ${{ matrix.db-version }} - wp: ${{ matrix.wp }} - new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} - multisite: ${{ matrix.multisite }} - - # Tests 5.x releases where the WordPress database version changed on the only supported version of PHP 7. - upgrade-tests-wp-5x-php-7x-mysql: - name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} - uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ 'ubuntu-24.04' ] - php: [ '7.4' ] - db-type: [ 'mysql' ] - db-version: [ '5.7', '8.4' ] - wp: [ '5.0', '5.1', '5.3', '5.4', '5.5', '5.6', '5.9' ] - multisite: [ false, true ] - with: - os: ${{ matrix.os }} - php: ${{ matrix.php }} - db-type: ${{ matrix.db-type }} - db-version: ${{ matrix.db-version }} - wp: ${{ matrix.wp }} - new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} - multisite: ${{ matrix.multisite }} - - # Tests 5.x releases where the WordPress database version changed on the oldest and newest supported versions of PHP 8. - # - # WordPress 5.0-5.2 are excluded from PHP 8+ testing because of the following fatal errors: - # - Use of __autoload(). - # - array/string offset with curly braces. - upgrade-tests-wp-5x-php-8x-mysql: - name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} - uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ 'ubuntu-24.04' ] - php: [ '8.0', '8.4' ] - db-type: [ 'mysql' ] - db-version: [ '5.7', '8.4' ] - wp: [ '5.3', '5.4', '5.5', '5.6', '5.9' ] - multisite: [ false, true ] - with: - os: ${{ matrix.os }} - php: ${{ matrix.php }} - db-type: ${{ matrix.db-type }} - db-version: ${{ matrix.db-version }} - wp: ${{ matrix.wp }} - new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} - multisite: ${{ matrix.multisite }} - - # The oldest version of WordPress receiving security updates should always be tested against - # the widest possible list of PHP/MySQL combinations. - # - # WordPress 4.7 is excluded from PHP 8+ testing because of the following fatal errors: - # - Use of __autoload(). - # - array/string offset with curly braces. - upgrade-tests-oldest-wp-mysql: - name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} - uses: ./.github/workflows/reusable-upgrade-testing.yml - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - os: [ 'ubuntu-24.04' ] - php: [ '7.4' ] - db-type: [ 'mysql' ] - db-version: [ '5.7', '8.0', '8.4', '9.6' ] - wp: [ '4.7' ] - multisite: [ false, true ] - with: - os: ${{ matrix.os }} - php: ${{ matrix.php }} - db-type: ${{ matrix.db-type }} - db-version: ${{ matrix.db-version }} - wp: ${{ matrix.wp }} - new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} - multisite: ${{ matrix.multisite }} - - slack-notifications: - name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml - permissions: - actions: read - contents: read - needs: [ upgrade-tests-recent-releases, upgrade-tests-wp-6x-mysql, upgrade-tests-wp-5x-php-7x-mysql, upgrade-tests-wp-5x-php-8x-mysql, upgrade-tests-oldest-wp-mysql ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-24.04 - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: `${context.runId}`, - } - }); diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 0aae098543e21..774d621151408 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -32,7 +32,7 @@ permissions: {} jobs: lint: name: Lint GitHub Action files - uses: ./.github/workflows/reusable-workflow-lint.yml + uses: WordPress/wordpress-develop/.github/workflows/reusable-workflow-lint.yml@trunk if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} permissions: security-events: write @@ -41,7 +41,7 @@ jobs: slack-notifications: name: Slack Notifications - uses: ./.github/workflows/slack-notifications.yml + uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk permissions: actions: read contents: read diff --git a/docker-compose.yml b/docker-compose.yml index cc2ed8d94975e..00fcb3e8c2850 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: # The PHP container. ## php: - image: wordpressdevelop/php:${LOCAL_PHP-latest} + image: wordpressdevelop/php:${LOCAL_PHP-8.5-fpm} networks: - wpdevnet @@ -64,7 +64,7 @@ services: # The MySQL container. ## mysql: - image: ${LOCAL_DB_TYPE-mysql}:${LOCAL_DB_VERSION-latest} + image: ${LOCAL_DB_TYPE-mysql}:${LOCAL_DB_VERSION-8.4} networks: - wpdevnet @@ -92,7 +92,7 @@ services: # The WP CLI container. ## cli: - image: wordpressdevelop/cli:${LOCAL_PHP-latest} + image: wordpressdevelop/cli:${LOCAL_PHP-8.5-fpm} networks: - wpdevnet From dcaa08f035b36cad9525701001e0170eced2308e Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Fri, 27 Mar 2026 16:19:24 +0000 Subject: [PATCH 03/57] Upgrade/Install: Use new default admin color scheme for language dropdown on the setup screen. This changeset ensures the hover/focus color of the setup screen's language dropdown use the new default admin color scheme. Reviewed by SergeyBiryukov. Merges [62163] to the 7.0 branch. Props huzaifaalmesbah, noruzzaman. Fixes #64961. See #64308. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62164 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/install.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/css/install.css b/src/wp-admin/css/install.css index 4173e9a228fda..71ea71c7d2863 100644 --- a/src/wp-admin/css/install.css +++ b/src/wp-admin/css/install.css @@ -340,7 +340,7 @@ body.language-chooser { .language-chooser select option:hover, .language-chooser select option:focus { - color: #0a4b78; + color: var(--wp-admin-theme-color-darker-20); } .language-chooser .step { From e53054bce1c21a05747a74fa2d33c20915049194 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Fri, 27 Mar 2026 22:50:06 +0000 Subject: [PATCH 04/57] Exports: Exclude `wp_sync_storage` post type from exports. Configured the Real Time Collaboration post type to be excluded from exports by default. The data is considered ephemeral and includes data on post IDs that may not match the IDs of posts on the importing site. Introduces a test to the export test suite to ensure that post types set to be excluded from exports are, in fact, excluded from exports. Reviewed by westonruter, desrosj, jorbin. Merges r62168 to the 7.0 branch. Props peterwilsoncc, desrosj, westonruter, jorbin, mukesh27, czarate. Fixes #64964. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62169 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/post.php | 1 + tests/phpunit/tests/admin/exportWp.php | 37 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 88deb1090fc5c..215fa153f7495 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -687,6 +687,7 @@ function create_initial_post_types() { 'show_in_menu' => false, 'show_in_rest' => false, 'show_ui' => false, + 'can_export' => false, 'supports' => array( 'custom-fields' ), ) ); diff --git a/tests/phpunit/tests/admin/exportWp.php b/tests/phpunit/tests/admin/exportWp.php index 11c615af6f497..f17ef0d4ad343 100644 --- a/tests/phpunit/tests/admin/exportWp.php +++ b/tests/phpunit/tests/admin/exportWp.php @@ -474,4 +474,41 @@ public function test_export_with_null_term_meta_values() { $this->assertNotFalse( $xml, 'Export should not fail with NULL term meta values' ); $this->assertGreaterThan( 0, count( $xml->channel->item ), 'Export should contain items' ); } + + /** + * Ensure that posts types with 'can_export' set to false are not included in the export. + * + * @ticket 64964 + */ + public function test_export_does_not_include_excluded_post_types() { + register_post_type( + 'wpexport_excluded', + array( 'can_export' => false ) + ); + + $excluded_post_id = self::factory()->post->create( + array( + 'post_title' => 'Excluded Post Type', + 'post_type' => 'wpexport_excluded', + 'post_status' => 'publish', + ) + ); + + $xml = $this->get_the_export( + array( + 'content' => 'all', + ) + ); + + $found_post = false; + foreach ( $xml->channel->item as $item ) { + $wp_item = $item->children( 'wp', true ); + if ( (int) $wp_item->post_id === $excluded_post_id ) { + $found_post = true; + break; + } + } + + $this->assertFalse( $found_post, 'Posts of excluded post types should not be included in export' ); + } } From 5c36141581324fe477513d8168a0e007a4e2302a Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Sat, 28 Mar 2026 16:35:23 +0000 Subject: [PATCH 05/57] Admin reskin: Remove line-height from input fields. `line-height` values that were previously used to match the height of input fields affect the height of the background shown when text inside those fields is selected. Removing these `line-height` declarations allows the text selection highlight to render more naturally. Additionally, update the height of the custom Date/Time format input fields on the General Settings screen to `32px` to align with the new design system. Reviewed by wildworks, audrasjb. Merges [62171] to the 7.0 branch. Props arkaprabhachowdhury, audrasjb, hmrisad, huzaifaalmesbah, manhar, manishxdp, noruzzaman, ozgursar, r1k0, sachinrajcp123, wildworks. Fixes #64763. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62172 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/color-picker.css | 1 - src/wp-admin/css/common.css | 1 - src/wp-admin/css/customize-controls.css | 1 - src/wp-admin/css/dashboard.css | 1 - src/wp-admin/css/forms.css | 6 +----- src/wp-admin/css/list-tables.css | 2 -- src/wp-admin/css/media.css | 1 - 7 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/wp-admin/css/color-picker.css b/src/wp-admin/css/color-picker.css index 2e038353b7cca..1e7525799e855 100644 --- a/src/wp-admin/css/color-picker.css +++ b/src/wp-admin/css/color-picker.css @@ -94,7 +94,6 @@ width: 4rem; font-size: 12px; font-family: monospace; - line-height: 2.33333333; /* 28px */ margin: 0; padding: 0 5px; vertical-align: top; diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index f3128b9e657ca..211cf0022c1e0 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1115,7 +1115,6 @@ th.action-links { .wp-filter .search-form input[type="search"] { min-height: 32px; - line-height: 2.14285714; /* 30px for 32px height with 14px font */ padding: 0 8px; } diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index 9e7b4d3185eba..2b4e87daa7ce7 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -2184,7 +2184,6 @@ p.customize-section-description { } .themes-filter-bar .wp-filter-search { - line-height: 2.14285714; /* 30px for 32px compact input */ padding: 0 10px 0 30px; max-width: 100%; width: 40%; diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index 562e730d026a1..324637a7a7b08 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -533,7 +533,6 @@ width: 40%; margin: 0; min-height: 32px; - line-height: 2.14285714; padding: 0 8px; } diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 35d67a9bdb666..e4e09ca1b6023 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -69,7 +69,6 @@ input[type="url"], input[type="week"] { padding: 0 12px; /* inherits font size 14px */ - line-height: 2.71428571; /* 38px for 40px min-height */ min-height: 40px; } @@ -816,7 +815,6 @@ p.search-box { p.search-box input[type="search"], p.search-box input[type="text"] { min-height: 32px; - line-height: 2.14285714; /* 30px for 32px height with 14px font */ padding: 0 8px; } @@ -1237,8 +1235,7 @@ table.form-table td .updated p { .options-general-php input.small-text { width: 56px; margin: -2px 0; - min-height: 24px; - line-height: 1.71428571; /* 24px for 14px font size */ + min-height: 32px; } .options-general-php .spinner { @@ -1601,7 +1598,6 @@ table.form-table td .updated p { -webkit-appearance: none; padding: 0 12px; min-height: 40px; - line-height: 2.5; /* 40px for 16px font */ } ::-webkit-datetime-edit { diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index e2b7e30f1dd63..2108f4644b406 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -686,7 +686,6 @@ th.sorted a span { font-size: 13px; text-align: center; min-height: 32px; - line-height: 2.30769231; /* 30px for 32px height with 13px font */ padding: 0 8px; } @@ -1099,7 +1098,6 @@ tr.inline-edit-row td { .inline-edit-row select, .inline-edit-row input:where(:not([type=checkbox],[type=radio],[type=submit],[type=button])) { - line-height: 2.14285714; min-height: 32px; padding: 0 8px 0 8px; } diff --git a/src/wp-admin/css/media.css b/src/wp-admin/css/media.css index 13378c2cafbaa..20806972d3aa1 100644 --- a/src/wp-admin/css/media.css +++ b/src/wp-admin/css/media.css @@ -568,7 +568,6 @@ border color while dragging a file over the uploader drop area */ .media-frame.mode-grid .media-toolbar input[type="search"] { min-height: 32px; - line-height: 2.14285714; /* 30px for 32px height with 14px font */ padding: 0 8px; } From e96c8c0a793af337ba53781c18331ffd697d9be0 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Wed, 1 Apr 2026 07:33:34 +0000 Subject: [PATCH 06/57] Administration: Prevent horizontal scrollbar in contextual help panel. In [62145], an ::after CSS rule was added that caused an overflow, resulting in an unintended scrollbar always appearing on Windows OS for example. This changeset removes the related CSS rule which is unnecessary to fix the initial issue. Follow-up to [62145]. Reviewed by wildworks, SergeyBiryukov. Merges [62187] to the 7.0 branch. Props wildworks, SergeyBiryukov, sabernhardt, audrasjb, huzaifaalmesbah, mehrazmorshed, mukesh27. Fixes #64744. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62190 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 211cf0022c1e0..c691383019f6d 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -2077,17 +2077,6 @@ p.auto-update-status { box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02), 0 1px 0 rgba(0, 0, 0, 0.02); } -.contextual-help-tabs .active::after { - content: ""; - position: absolute; - top: 0; - right: -1px; - width: 2px; - height: 100%; - background: inherit; - z-index: 2; -} - .contextual-help-tabs .active a { border-color: #c3c4c7; color: #2c3338; From af57e89d8e6c3d60cbdfb6c8ebd3993cb505b267 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 1 Apr 2026 16:37:48 +0000 Subject: [PATCH 07/57] Connectors: Fix and generalize the API for custom connector types. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validate `setting_name`, `constant_name`, and `env_var_name` in connector registration — reject invalid values with `_doing_it_wrong()` instead of silently falling back. Change the auto-generated `setting_name` pattern from `connectors_ai_{$id}_api_key` to `connectors_{$type}_{$id}_api_key` so it works for any connector type. Built-in AI providers infer their names using the existing `connectors_ai_{$id}_api_key` convention, preserving backward compatibility. Add `constant_name` and `env_var_name` as optional authentication fields, allowing connectors to declare explicit PHP constant and environment variable names for API key lookup. AI providers auto-generate these using the `{CONSTANT_CASE_ID}_API_KEY` convention. Refactor `_wp_connectors_get_api_key_source()` to accept explicit `env_var_name` and `constant_name` parameters instead of deriving them from the provider ID. Environment variable and constant checks are skipped when not provided. Generalize REST dispatch, settings registration, and script module data to work with all connector types, not just `ai_provider`. Settings registration skips already-registered settings. Non-AI connectors determine `isConnected` based on key source. Replace `isInstalled` with `pluginFile` in script module data output to fix plugin entity ID resolution on the frontend. Update PHPDoc to reflect current behavior — widen `type` from literal `'ai_provider'` to `non-empty-string`, document new authentication fields, and use Anthropic examples throughout. Props gziolo, jorgefilipecosta. Fixes #64957. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62194 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-connector-registry.php | 65 +++++-- src/wp-includes/connectors.php | 134 ++++++++----- .../wp-ai-client-mock-provider-trait.php | 1 + .../tests/connectors/wpConnectorRegistry.php | 182 ++++++++++++++++-- .../wpConnectorsGetApiKeySource.php | 141 ++++++++++++++ 5 files changed, 451 insertions(+), 72 deletions(-) create mode 100644 tests/phpunit/tests/connectors/wpConnectorsGetApiKeySource.php diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index a4bd8fb48f29b..18a5f80c94dbd 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -31,11 +31,13 @@ * name: non-empty-string, * description: non-empty-string, * logo_url?: non-empty-string, - * type: 'ai_provider', + * type: non-empty-string, * authentication: array{ * method: 'api_key'|'none', * credentials_url?: non-empty-string, - * setting_name?: non-empty-string + * setting_name?: non-empty-string, + * constant_name?: non-empty-string, + * env_var_name?: non-empty-string * }, * plugin?: array{ * slug: non-empty-string @@ -66,12 +68,12 @@ final class WP_Connector_Registry { * Registers a new connector. * * Validates the provided arguments and stores the connector in the registry. - * For connectors with `api_key` authentication, a `setting_name` is automatically - * generated using the pattern `connectors_ai_{$id}_api_key`, with hyphens in the ID - * normalized to underscores (e.g., connector ID `openai` produces - * `connectors_ai_openai_api_key`, and `azure-openai` produces - * `connectors_ai_azure_openai_api_key`). This setting name is used for the Settings - * API registration and REST API exposure. + * For connectors with `api_key` authentication, a `setting_name` can be provided + * explicitly. If omitted, one is automatically generated using the pattern + * `connectors_{$type}_{$id}_api_key`, with hyphens in the type and ID normalized + * to underscores (e.g., connector type `spam_filtering` with ID `akismet` produces + * `connectors_spam_filtering_akismet_api_key`). This setting name is used for the + * Settings API registration and REST API exposure. * * Registering a connector with an ID that is already registered will trigger a * `_doing_it_wrong()` notice and return `null`. To override an existing connector, @@ -89,12 +91,20 @@ final class WP_Connector_Registry { * @type string $name Required. The connector's display name. * @type string $description Optional. The connector's description. Default empty string. * @type string $logo_url Optional. URL to the connector's logo image. - * @type string $type Required. The connector type. Currently, only 'ai_provider' is supported. + * @type string $type Required. The connector type, e.g. 'ai_provider'. * @type array $authentication { * Required. Authentication configuration. * * @type string $method Required. The authentication method: 'api_key' or 'none'. * @type string $credentials_url Optional. URL where users can obtain API credentials. + * @type string $setting_name Optional. The setting name for the API key. + * When omitted, auto-generated as + * `connectors_{$type}_{$id}_api_key`. + * Must be a non-empty string when provided. + * @type string $constant_name Optional. PHP constant name for the API key + * (e.g. 'ANTHROPIC_API_KEY'). Only checked when provided. + * @type string $env_var_name Optional. Environment variable name for the API key + * (e.g. 'ANTHROPIC_API_KEY'). Only checked when provided. * } * @type array $plugin { * Optional. Plugin data for install/activate UI. @@ -192,10 +202,43 @@ public function register( string $id, array $args ): ?array { if ( ! empty( $args['authentication']['credentials_url'] ) && is_string( $args['authentication']['credentials_url'] ) ) { $connector['authentication']['credentials_url'] = $args['authentication']['credentials_url']; } - if ( ! empty( $args['authentication']['setting_name'] ) && is_string( $args['authentication']['setting_name'] ) ) { + if ( isset( $args['authentication']['setting_name'] ) ) { + if ( ! is_string( $args['authentication']['setting_name'] ) || '' === $args['authentication']['setting_name'] ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Connector ID. */ + sprintf( __( 'Connector "%s" authentication setting_name must be a non-empty string.' ), esc_html( $id ) ), + '7.0.0' + ); + return null; + } $connector['authentication']['setting_name'] = $args['authentication']['setting_name']; } else { - $connector['authentication']['setting_name'] = 'connectors_ai_' . str_replace( '-', '_', $id ) . '_api_key'; + $connector['authentication']['setting_name'] = str_replace( '-', '_', "connectors_{$connector['type']}_{$id}_api_key" ); + } + if ( isset( $args['authentication']['constant_name'] ) ) { + if ( ! is_string( $args['authentication']['constant_name'] ) || '' === $args['authentication']['constant_name'] ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Connector ID. */ + sprintf( __( 'Connector "%s" authentication constant_name must be a non-empty string.' ), esc_html( $id ) ), + '7.0.0' + ); + return null; + } + $connector['authentication']['constant_name'] = $args['authentication']['constant_name']; + } + if ( isset( $args['authentication']['env_var_name'] ) ) { + if ( ! is_string( $args['authentication']['env_var_name'] ) || '' === $args['authentication']['env_var_name'] ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Connector ID. */ + sprintf( __( 'Connector "%s" authentication env_var_name must be a non-empty string.' ), esc_html( $id ) ), + '7.0.0' + ); + return null; + } + $connector['authentication']['env_var_name'] = $args['authentication']['env_var_name']; } } diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index bdc585723aaf1..06683ccaaa25c 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -43,14 +43,17 @@ function wp_is_connector_registered( string $id ): bool { * @type string $name The connector's display name. * @type string $description The connector's description. * @type string $logo_url Optional. URL to the connector's logo image. - * @type string $type The connector type. Currently, only 'ai_provider' is supported. + * @type string $type The connector type, e.g. 'ai_provider'. * @type array $authentication { * Authentication configuration. When method is 'api_key', includes - * credentials_url and setting_name. When 'none', only method is present. + * credentials_url, setting_name, and optionally constant_name and + * env_var_name. When 'none', only method is present. * * @type string $method The authentication method: 'api_key' or 'none'. * @type string $credentials_url Optional. URL where users can obtain API credentials. * @type string $setting_name Optional. The setting name for the API key. + * @type string $constant_name Optional. PHP constant name for the API key. + * @type string $env_var_name Optional. Environment variable name for the API key. * } * @type array $plugin { * Optional. Plugin data for install/activate UI. @@ -62,11 +65,13 @@ function wp_is_connector_registered( string $id ): bool { * name: non-empty-string, * description: non-empty-string, * logo_url?: non-empty-string, - * type: 'ai_provider', + * type: non-empty-string, * authentication: array{ * method: 'api_key'|'none', * credentials_url?: non-empty-string, - * setting_name?: non-empty-string + * setting_name?: non-empty-string, + * constant_name?: non-empty-string, + * env_var_name?: non-empty-string * }, * plugin?: array{ * slug: non-empty-string @@ -98,14 +103,17 @@ function wp_get_connector( string $id ): ?array { * @type string $name The connector's display name. * @type string $description The connector's description. * @type string $logo_url Optional. URL to the connector's logo image. - * @type string $type The connector type. Currently, only 'ai_provider' is supported. + * @type string $type The connector type, e.g. 'ai_provider'. * @type array $authentication { * Authentication configuration. When method is 'api_key', includes - * credentials_url and setting_name. When 'none', only method is present. + * credentials_url, setting_name, and optionally constant_name and + * env_var_name. When 'none', only method is present. * * @type string $method The authentication method: 'api_key' or 'none'. * @type string $credentials_url Optional. URL where users can obtain API credentials. * @type string $setting_name Optional. The setting name for the API key. + * @type string $constant_name Optional. PHP constant name for the API key. + * @type string $env_var_name Optional. Environment variable name for the API key. * } * @type array $plugin { * Optional. Plugin data for install/activate UI. @@ -118,11 +126,13 @@ function wp_get_connector( string $id ): ?array { * name: non-empty-string, * description: non-empty-string, * logo_url?: non-empty-string, - * type: 'ai_provider', + * type: non-empty-string, * authentication: array{ * method: 'api_key'|'none', * credentials_url?: non-empty-string, - * setting_name?: non-empty-string + * setting_name?: non-empty-string, + * constant_name?: non-empty-string, + * env_var_name?: non-empty-string * }, * plugin?: array{ * slug: non-empty-string @@ -216,10 +226,10 @@ function _wp_connectors_init(): void { * Example — overriding metadata on an auto-discovered connector: * * add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) { - * if ( $registry->is_registered( 'openai' ) ) { - * $connector = $registry->unregister( 'openai' ); - * $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' ); - * $registry->register( 'openai', $connector ); + * if ( $registry->is_registered( 'anthropic' ) ) { + * $connector = $registry->unregister( 'anthropic' ); + * $connector['description'] = __( 'Custom description for Anthropic.', 'my-plugin' ); + * $registry->register( 'anthropic', $connector ); * } * } ); * @@ -335,6 +345,26 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re // Register all default connectors directly on the registry. foreach ( $defaults as $id => $args ) { + if ( 'api_key' === $args['authentication']['method'] ) { + $sanitized_id = str_replace( '-', '_', $id ); + + if ( ! isset( $args['authentication']['setting_name'] ) ) { + $args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key"; + } + + // All AI providers use the {CONSTANT_CASE_ID}_API_KEY naming convention. + if ( ! isset( $args['authentication']['constant_name'] ) || ! isset( $args['authentication']['env_var_name'] ) ) { + $constant_case_key = strtoupper( preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY'; + + if ( ! isset( $args['authentication']['constant_name'] ) ) { + $args['authentication']['constant_name'] = $constant_case_key; + } + + if ( ! isset( $args['authentication']['env_var_name'] ) ) { + $args['authentication']['env_var_name'] = $constant_case_key; + } + } + } $registry->register( $id, $args ); } } @@ -357,35 +387,32 @@ function _wp_connectors_mask_api_key( string $key ): string { } /** - * Determines the source of an API key for a given provider. + * Determines the source of an API key for a given connector. * * Checks in order: environment variable, PHP constant, database. - * Uses the same naming convention as the WP AI Client ProviderRegistry. + * Environment variable and constant are only checked when their + * respective names are provided. * * @since 7.0.0 * @access private * - * @param string $provider_id The provider ID (e.g., 'openai', 'anthropic', 'google'). - * @param string $setting_name The option name for the API key (e.g., 'connectors_ai_openai_api_key'). + * @param string $setting_name The option name for the API key (e.g., 'connectors_spam_filtering_akismet_api_key'). + * @param string $env_var_name Optional. Environment variable name to check (e.g., 'AKISMET_API_KEY'). + * @param string $constant_name Optional. PHP constant name to check (e.g., 'AKISMET_API_KEY'). * @return string The key source: 'env', 'constant', 'database', or 'none'. */ -function _wp_connectors_get_api_key_source( string $provider_id, string $setting_name ): string { - // Convert provider ID to CONSTANT_CASE for env var name. - // e.g., 'openai' -> 'OPENAI', 'anthropic' -> 'ANTHROPIC'. - $constant_case_id = strtoupper( - preg_replace( '/([a-z])([A-Z])/', '$1_$2', str_replace( '-', '_', $provider_id ) ) - ); - $env_var_name = "{$constant_case_id}_API_KEY"; - +function _wp_connectors_get_api_key_source( string $setting_name, string $env_var_name = '', string $constant_name = '' ): string { // Check environment variable first. - $env_value = getenv( $env_var_name ); - if ( false !== $env_value && '' !== $env_value ) { - return 'env'; + if ( '' !== $env_var_name ) { + $env_value = getenv( $env_var_name ); + if ( false !== $env_value && '' !== $env_value ) { + return 'env'; + } } // Check PHP constant. - if ( defined( $env_var_name ) ) { - $const_value = constant( $env_var_name ); + if ( '' !== $constant_name && defined( $constant_name ) ) { + $const_value = constant( $constant_name ); if ( is_string( $const_value ) && '' !== $const_value ) { return 'constant'; } @@ -470,7 +497,7 @@ function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_R foreach ( wp_get_connectors() as $connector_id => $connector_data ) { $auth = $connector_data['authentication']; - if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { + if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { continue; } @@ -481,8 +508,9 @@ function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_R $value = $data[ $setting_name ]; - // On update, validate the key before masking. - if ( $is_update && is_string( $value ) && '' !== $value ) { + // On update, validate AI provider keys before masking. + // Non-AI connectors accept keys as-is; the service plugin handles its own validation. + if ( $is_update && is_string( $value ) && '' !== $value && 'ai_provider' === $connector_data['type'] ) { if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) { update_option( $setting_name, '' ); $data[ $setting_name ] = ''; @@ -508,16 +536,22 @@ function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_R * @access private */ function _wp_register_default_connector_settings(): void { - $ai_registry = AiClient::defaultRegistry(); + $ai_registry = AiClient::defaultRegistry(); + $registered_settings = get_registered_settings(); foreach ( wp_get_connectors() as $connector_id => $connector_data ) { $auth = $connector_data['authentication']; - if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { + if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { + continue; + } + + // Skip if the setting is already registered (e.g. by an owning plugin). + if ( isset( $registered_settings[ $auth['setting_name'] ] ) ) { continue; } - // Skip registering the setting if the provider is not in the registry. - if ( ! $ai_registry->hasProvider( $connector_id ) ) { + // For AI providers, skip if the provider is not in the AI Client registry. + if ( 'ai_provider' === $connector_data['type'] && ! $ai_registry->hasProvider( $connector_id ) ) { continue; } @@ -527,13 +561,13 @@ function _wp_register_default_connector_settings(): void { array( 'type' => 'string', 'label' => sprintf( - /* translators: %s: AI provider name. */ + /* translators: %s: Connector name. */ __( '%s API Key' ), $connector_data['name'] ), 'description' => sprintf( - /* translators: %s: AI provider name. */ - __( 'API key for the %s AI provider.' ), + /* translators: %s: Connector name. */ + __( 'API key for the %s connector.' ), $connector_data['name'] ), 'default' => '', @@ -569,7 +603,7 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { } // Skip if the key is already provided via env var or constant. - $key_source = _wp_connectors_get_api_key_source( $connector_id, $auth['setting_name'] ); + $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'], $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' ); if ( 'env' === $key_source || 'constant' === $key_source ) { continue; } @@ -620,11 +654,17 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { if ( 'api_key' === $auth['method'] ) { $auth_out['settingName'] = $auth['setting_name'] ?? ''; $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; - $auth_out['keySource'] = _wp_connectors_get_api_key_source( $connector_id, $auth['setting_name'] ?? '' ); - try { - $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id ); - } catch ( Exception $e ) { - $auth_out['isConnected'] = false; + $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'] ?? '', $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' ); + $auth_out['keySource'] = $key_source; + + if ( 'ai_provider' === $connector_data['type'] ) { + try { + $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id ); + } catch ( Exception $e ) { + $auth_out['isConnected'] = false; + } + } else { + $auth_out['isConnected'] = 'none' !== $key_source; } } @@ -645,7 +685,9 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { $connector_out['plugin'] = array( 'slug' => $plugin_slug, - 'isInstalled' => $is_installed, + 'pluginFile' => $is_installed + ? ( str_ends_with( $plugin_file, '.php' ) ? substr( $plugin_file, 0, -4 ) : $plugin_file ) + : null, 'isActivated' => $is_activated, ); } diff --git a/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php b/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php index e7b88025aa592..42f85c212d092 100644 --- a/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php +++ b/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php @@ -172,6 +172,7 @@ private static function register_mock_connectors_provider(): void { 'authentication' => array( 'method' => 'api_key', 'credentials_url' => null, + 'setting_name' => 'connectors_ai_mock_connectors_test_api_key', ), ) ); diff --git a/tests/phpunit/tests/connectors/wpConnectorRegistry.php b/tests/phpunit/tests/connectors/wpConnectorRegistry.php index 522c9f9299ddb..cab030d930dcd 100644 --- a/tests/phpunit/tests/connectors/wpConnectorRegistry.php +++ b/tests/phpunit/tests/connectors/wpConnectorRegistry.php @@ -32,9 +32,9 @@ public function set_up(): void { $this->registry = new WP_Connector_Registry(); self::$default_args = array( - 'name' => 'Test Provider', - 'description' => 'A test AI provider.', - 'type' => 'ai_provider', + 'name' => 'Test Connector', + 'description' => 'A test connector.', + 'type' => 'test_type', 'authentication' => array( 'method' => 'api_key', 'credentials_url' => 'https://example.com/keys', @@ -49,12 +49,12 @@ public function test_register_returns_connector_data() { $result = $this->registry->register( 'test-provider', self::$default_args ); $this->assertIsArray( $result ); - $this->assertSame( 'Test Provider', $result['name'] ); - $this->assertSame( 'A test AI provider.', $result['description'] ); - $this->assertSame( 'ai_provider', $result['type'] ); + $this->assertSame( 'Test Connector', $result['name'] ); + $this->assertSame( 'A test connector.', $result['description'] ); + $this->assertSame( 'test_type', $result['type'] ); $this->assertSame( 'api_key', $result['authentication']['method'] ); $this->assertSame( 'https://example.com/keys', $result['authentication']['credentials_url'] ); - $this->assertSame( 'connectors_ai_test_provider_api_key', $result['authentication']['setting_name'] ); + $this->assertSame( 'connectors_test_type_test_provider_api_key', $result['authentication']['setting_name'] ); } /** @@ -63,7 +63,7 @@ public function test_register_returns_connector_data() { public function test_register_generates_setting_name_for_api_key() { $result = $this->registry->register( 'myai', self::$default_args ); - $this->assertSame( 'connectors_ai_myai_api_key', $result['authentication']['setting_name'] ); + $this->assertSame( 'connectors_test_type_myai_api_key', $result['authentication']['setting_name'] ); } /** @@ -72,7 +72,157 @@ public function test_register_generates_setting_name_for_api_key() { public function test_register_generates_setting_name_normalizes_hyphens() { $result = $this->registry->register( 'my-ai', self::$default_args ); - $this->assertSame( 'connectors_ai_my_ai_api_key', $result['authentication']['setting_name'] ); + $this->assertSame( 'connectors_test_type_my_ai_api_key', $result['authentication']['setting_name'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_generates_setting_name_using_type_and_id() { + $args = self::$default_args; + $args['type'] = 'email_delivery'; + + $result = $this->registry->register( 'sendgrid', $args ); + + $this->assertSame( 'connectors_email_delivery_sendgrid_api_key', $result['authentication']['setting_name'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_uses_custom_setting_name_when_provided() { + $args = self::$default_args; + $args['authentication']['setting_name'] = 'wordpress_api_key'; + + $result = $this->registry->register( 'custom-setting', $args ); + + $this->assertSame( 'wordpress_api_key', $result['authentication']['setting_name'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_rejects_empty_setting_name() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['authentication']['setting_name'] = ''; + + $result = $this->registry->register( 'empty-setting', $args ); + + $this->assertNull( $result ); + } + + /** + * @ticket 64957 + */ + public function test_register_rejects_non_string_setting_name() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['authentication']['setting_name'] = 123; + + $result = $this->registry->register( 'non-string-setting', $args ); + + $this->assertNull( $result ); + } + + /** + * @ticket 64957 + */ + public function test_register_stores_constant_name_when_provided() { + $args = self::$default_args; + $args['authentication']['constant_name'] = 'MY_PROVIDER_API_KEY'; + + $result = $this->registry->register( 'my-provider', $args ); + + $this->assertSame( 'MY_PROVIDER_API_KEY', $result['authentication']['constant_name'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_omits_constant_name_when_not_provided() { + $result = $this->registry->register( 'no-const', self::$default_args ); + + $this->assertArrayNotHasKey( 'constant_name', $result['authentication'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_rejects_empty_constant_name() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['authentication']['constant_name'] = ''; + + $result = $this->registry->register( 'empty-const', $args ); + + $this->assertNull( $result ); + } + + /** + * @ticket 64957 + */ + public function test_register_rejects_non_string_constant_name() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['authentication']['constant_name'] = 123; + + $result = $this->registry->register( 'bad-const', $args ); + + $this->assertNull( $result ); + } + + /** + * @ticket 64957 + */ + public function test_register_stores_env_var_name_when_provided() { + $args = self::$default_args; + $args['authentication']['env_var_name'] = 'MY_PROVIDER_API_KEY'; + + $result = $this->registry->register( 'my-provider', $args ); + + $this->assertSame( 'MY_PROVIDER_API_KEY', $result['authentication']['env_var_name'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_omits_env_var_name_when_not_provided() { + $result = $this->registry->register( 'no-env', self::$default_args ); + + $this->assertArrayNotHasKey( 'env_var_name', $result['authentication'] ); + } + + /** + * @ticket 64957 + */ + public function test_register_rejects_empty_env_var_name() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['authentication']['env_var_name'] = ''; + + $result = $this->registry->register( 'empty-env', $args ); + + $this->assertNull( $result ); + } + + /** + * @ticket 64957 + */ + public function test_register_rejects_non_string_env_var_name() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['authentication']['env_var_name'] = 123; + + $result = $this->registry->register( 'bad-env', $args ); + + $this->assertNull( $result ); } /** @@ -80,8 +230,8 @@ public function test_register_generates_setting_name_normalizes_hyphens() { */ public function test_register_no_setting_name_for_none_auth() { $args = array( - 'name' => 'No Auth Provider', - 'type' => 'ai_provider', + 'name' => 'No Auth Connector', + 'type' => 'test_type', 'authentication' => array( 'method' => 'none' ), ); $result = $this->registry->register( 'no-auth', $args ); @@ -96,7 +246,7 @@ public function test_register_no_setting_name_for_none_auth() { public function test_register_defaults_description_to_empty_string() { $args = array( 'name' => 'Minimal', - 'type' => 'ai_provider', + 'type' => 'test_type', 'authentication' => array( 'method' => 'none' ), ); @@ -308,7 +458,7 @@ public function test_get_registered_returns_connector_data() { $result = $this->registry->get_registered( 'my-connector' ); $this->assertIsArray( $result ); - $this->assertSame( 'Test Provider', $result['name'] ); + $this->assertSame( 'Test Connector', $result['name'] ); } /** @@ -355,7 +505,7 @@ public function test_unregister_removes_connector() { $result = $this->registry->unregister( 'to-remove' ); $this->assertIsArray( $result ); - $this->assertSame( 'Test Provider', $result['name'] ); + $this->assertSame( 'Test Connector', $result['name'] ); $this->assertFalse( $this->registry->is_registered( 'to-remove' ) ); } @@ -404,7 +554,9 @@ public function test_get_instance_returns_same_instance() { public function test_register_skips_when_ai_not_supported() { add_filter( 'wp_supports_ai', '__return_false' ); - $this->registry->register( 'first', self::$default_args ); + $args = self::$default_args; + $args['type'] = 'ai_provider'; + $this->registry->register( 'first', $args ); $all = $this->registry->get_all_registered(); $this->assertCount( 0, $all ); diff --git a/tests/phpunit/tests/connectors/wpConnectorsGetApiKeySource.php b/tests/phpunit/tests/connectors/wpConnectorsGetApiKeySource.php new file mode 100644 index 0000000000000..cdbb53b90ab09 --- /dev/null +++ b/tests/phpunit/tests/connectors/wpConnectorsGetApiKeySource.php @@ -0,0 +1,141 @@ +assertSame( 'none', $result ); + } + + /** + * @ticket 64957 + */ + public function test_returns_database_when_option_set() { + $setting_name = 'connectors_ai_test_source_api_key'; + update_option( $setting_name, 'sk-test-key-123' ); + + $result = _wp_connectors_get_api_key_source( $setting_name ); + + delete_option( $setting_name ); + + $this->assertSame( 'database', $result ); + } + + /** + * @ticket 64957 + */ + public function test_returns_env_when_env_var_set() { + $env_var = 'WP_TEST_CONNECTOR_API_KEY'; + putenv( "{$env_var}=sk-from-env" ); + + $result = _wp_connectors_get_api_key_source( 'connectors_ai_test_api_key', $env_var ); + + putenv( $env_var ); + + $this->assertSame( 'env', $result ); + } + + /** + * @ticket 64957 + */ + public function test_returns_constant_when_constant_defined() { + $constant_name = 'WP_TEST_CONNECTOR_CONST_KEY'; + if ( ! defined( $constant_name ) ) { + define( $constant_name, 'sk-from-constant' ); + } + + $result = _wp_connectors_get_api_key_source( 'connectors_ai_test_api_key', '', $constant_name ); + + $this->assertSame( 'constant', $result ); + } + + /** + * @ticket 64957 + */ + public function test_env_takes_priority_over_constant_and_database() { + $setting_name = 'connectors_ai_priority_test_api_key'; + $env_var = 'WP_TEST_PRIORITY_ENV_KEY'; + $constant_name = 'WP_TEST_PRIORITY_CONST_KEY'; + + update_option( $setting_name, 'sk-from-db' ); + putenv( "{$env_var}=sk-from-env" ); + if ( ! defined( $constant_name ) ) { + define( $constant_name, 'sk-from-constant' ); + } + + $result = _wp_connectors_get_api_key_source( $setting_name, $env_var, $constant_name ); + + putenv( $env_var ); + delete_option( $setting_name ); + + $this->assertSame( 'env', $result ); + } + + /** + * @ticket 64957 + */ + public function test_constant_takes_priority_over_database() { + $setting_name = 'connectors_ai_const_priority_api_key'; + $constant_name = 'WP_TEST_CONST_PRIORITY_KEY'; + + update_option( $setting_name, 'sk-from-db' ); + if ( ! defined( $constant_name ) ) { + define( $constant_name, 'sk-from-constant' ); + } + + $result = _wp_connectors_get_api_key_source( $setting_name, '', $constant_name ); + + delete_option( $setting_name ); + + $this->assertSame( 'constant', $result ); + } + + /** + * @ticket 64957 + */ + public function test_skips_env_check_when_env_var_name_empty() { + $env_var = 'WP_TEST_SKIP_ENV_KEY'; + $setting_name = 'connectors_ai_skip_env_api_key'; + + putenv( "{$env_var}=sk-from-env" ); + update_option( $setting_name, 'sk-from-db' ); + + // Empty env_var_name means env is not checked, falls through to database. + $result = _wp_connectors_get_api_key_source( $setting_name, '', '' ); + + putenv( $env_var ); + delete_option( $setting_name ); + + $this->assertSame( 'database', $result ); + } + + /** + * @ticket 64957 + */ + public function test_skips_constant_check_when_constant_name_empty() { + $constant_name = 'WP_TEST_SKIP_CONST_KEY'; + $setting_name = 'connectors_ai_skip_const_api_key'; + + if ( ! defined( $constant_name ) ) { + define( $constant_name, 'sk-from-constant' ); + } + update_option( $setting_name, 'sk-from-db' ); + + // Empty constant_name means constant is not checked, falls through to database. + $result = _wp_connectors_get_api_key_source( $setting_name, '' ); + + delete_option( $setting_name ); + + $this->assertSame( 'database', $result ); + } +} From 0ad27743d9822485ab64551651e8fc576ac7db54 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 1 Apr 2026 16:41:40 +0000 Subject: [PATCH 08/57] Connectors: Replace plugin.slug with plugin.file in connector registration. Use the plugin's main file path relative to the plugins directory (e.g. `akismet/akismet.php` or `hello.php`) instead of the WordPress.org slug to identify a connector's associated plugin. This lets `_wp_connectors_get_connector_script_module_data()` check plugin status with `file_exists()` and `is_plugin_active()` directly, removing the `get_plugins()` slug-to-file mapping that was previously needed. Props jorgefilipecosta, mukesh27, gziolo. Fixes #65002. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62195 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-connector-registry.php | 9 +++-- src/wp-includes/connectors.php | 40 ++++++++----------- .../tests/connectors/wpConnectorRegistry.php | 4 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index 18a5f80c94dbd..d7643360efeeb 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -40,7 +40,7 @@ * env_var_name?: non-empty-string * }, * plugin?: array{ - * slug: non-empty-string + * file: non-empty-string * } * } */ @@ -109,7 +109,8 @@ final class WP_Connector_Registry { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $slug The WordPress.org plugin slug. + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'akismet/akismet.php' or 'hello.php'). * } * } * @return array|null The registered connector data on success, null on failure. @@ -242,8 +243,8 @@ public function register( string $id, array $args ): ?array { } } - if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) { - $connector['plugin'] = $args['plugin']; + if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) && ! empty( $args['plugin']['file'] ) ) { + $connector['plugin'] = array( 'file' => $args['plugin']['file'] ); } $this->registered_connectors[ $id ] = $connector; diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 06683ccaaa25c..68c8b4c1570d0 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -58,7 +58,8 @@ function wp_is_connector_registered( string $id ): bool { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $slug The WordPress.org plugin slug. + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'akismet/akismet.php' or 'hello.php'). * } * } * @phpstan-return ?array{ @@ -74,7 +75,7 @@ function wp_is_connector_registered( string $id ): bool { * env_var_name?: non-empty-string * }, * plugin?: array{ - * slug: non-empty-string + * file: non-empty-string * } * } */ @@ -118,7 +119,8 @@ function wp_get_connector( string $id ): ?array { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $slug The WordPress.org plugin slug. + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'akismet/akismet.php' or 'hello.php'). * } * } * } @@ -135,7 +137,7 @@ function wp_get_connector( string $id ): ?array { * env_var_name?: non-empty-string * }, * plugin?: array{ - * slug: non-empty-string + * file: non-empty-string * } * }> */ @@ -256,7 +258,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re 'description' => __( 'Text generation with Claude.' ), 'type' => 'ai_provider', 'plugin' => array( - 'slug' => 'ai-provider-for-anthropic', + 'file' => 'ai-provider-for-anthropic/plugin.php', ), 'authentication' => array( 'method' => 'api_key', @@ -268,7 +270,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re 'description' => __( 'Text and image generation with Gemini and Imagen.' ), 'type' => 'ai_provider', 'plugin' => array( - 'slug' => 'ai-provider-for-google', + 'file' => 'ai-provider-for-google/plugin.php', ), 'authentication' => array( 'method' => 'api_key', @@ -280,7 +282,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re 'description' => __( 'Text and image generation with GPT and Dall-E.' ), 'type' => 'ai_provider', 'plugin' => array( - 'slug' => 'ai-provider-for-openai', + 'file' => 'ai-provider-for-openai/plugin.php', ), 'authentication' => array( 'method' => 'api_key', @@ -636,15 +638,9 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { function _wp_connectors_get_connector_script_module_data( array $data ): array { $registry = AiClient::defaultRegistry(); - // Build a slug-to-file map for plugin installation status. - if ( ! function_exists( 'get_plugins' ) ) { + if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - $plugin_files_by_slug = array(); - foreach ( array_keys( get_plugins() ) as $plugin_file ) { - $slug = str_contains( $plugin_file, '/' ) ? dirname( $plugin_file ) : str_replace( '.php', '', $plugin_file ); - $plugin_files_by_slug[ $slug ] = $plugin_file; - } $connectors = array(); foreach ( wp_get_connectors() as $connector_id => $connector_data ) { @@ -676,18 +672,14 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { 'authentication' => $auth_out, ); - if ( ! empty( $connector_data['plugin']['slug'] ) ) { - $plugin_slug = $connector_data['plugin']['slug']; - $plugin_file = $plugin_files_by_slug[ $plugin_slug ] ?? null; - - $is_installed = null !== $plugin_file; - $is_activated = $is_installed && is_plugin_active( $plugin_file ); + if ( ! empty( $connector_data['plugin']['file'] ) ) { + $file = $connector_data['plugin']['file']; + $is_installed = file_exists( wp_normalize_path( WP_PLUGIN_DIR . '/' . $file ) ); + $is_activated = $is_installed && is_plugin_active( $file ); $connector_out['plugin'] = array( - 'slug' => $plugin_slug, - 'pluginFile' => $is_installed - ? ( str_ends_with( $plugin_file, '.php' ) ? substr( $plugin_file, 0, -4 ) : $plugin_file ) - : null, + 'file' => $file, + 'isInstalled' => $is_installed, 'isActivated' => $is_activated, ); } diff --git a/tests/phpunit/tests/connectors/wpConnectorRegistry.php b/tests/phpunit/tests/connectors/wpConnectorRegistry.php index cab030d930dcd..d1a46dc0981fe 100644 --- a/tests/phpunit/tests/connectors/wpConnectorRegistry.php +++ b/tests/phpunit/tests/connectors/wpConnectorRegistry.php @@ -294,12 +294,12 @@ public function test_register_omits_logo_url_when_empty() { */ public function test_register_includes_plugin_data() { $args = self::$default_args; - $args['plugin'] = array( 'slug' => 'my-plugin' ); + $args['plugin'] = array( 'file' => 'my-plugin/my-plugin.php' ); $result = $this->registry->register( 'with-plugin', $args ); $this->assertArrayHasKey( 'plugin', $result ); - $this->assertSame( array( 'slug' => 'my-plugin' ), $result['plugin'] ); + $this->assertSame( array( 'file' => 'my-plugin/my-plugin.php' ), $result['plugin'] ); } /** From 94d74a13c31781a906adad3f8e3ac9be3f49b9f5 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Wed, 1 Apr 2026 22:14:27 +0000 Subject: [PATCH 09/57] Admin Reskin: Correct vertical alignment of pagination elements in list tables. Follow-up to [61645]. Reviewed by audrasjb, SergeyBiryukov. Merges [62182] to the 7.0 branch. Props TobiasBg, rcorrales, opurockey, rahultank, SergeyBiryukov. Fixes #64975. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62197 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/list-tables.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index 2108f4644b406..c659079bea2e9 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -682,6 +682,7 @@ th.sorted a span { } .tablenav-pages .current-page { + vertical-align: top; margin: 0 2px 0 0; font-size: 13px; text-align: center; From 85c772239e238d6f3155fe3484badd147cf97d52 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Fri, 3 Apr 2026 01:33:32 +0000 Subject: [PATCH 10/57] Admin Reskin: Change color picker height to match new design system. Update min-height from 30px to 32px for the color picker button and related elements to match new design system. Reviewed by joedolson, wildworks. Merges [62191] to the 7.0 branch. Props audrasjb, hmbashar, huzaifaalmesbah, joedolson, juanmaguitar, mukesh27, noruzzaman, ozgursar, rahultank, rcorrales, sajib1223, shailu25, tusharaddweb, vgnavada, wildworks. Fixes #64761. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62202 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/color-picker.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/css/color-picker.css b/src/wp-admin/css/color-picker.css index 1e7525799e855..8264432dd39cc 100644 --- a/src/wp-admin/css/color-picker.css +++ b/src/wp-admin/css/color-picker.css @@ -10,7 +10,7 @@ /* Needs higher specificity to override `.wp-core-ui .button`. */ .wp-picker-container .wp-color-result.button { - min-height: 30px; + min-height: 32px; margin: 0 6px 6px 0; padding: 0 0 0 30px; font-size: 11px; @@ -22,7 +22,7 @@ border-left: 1px solid #c3c4c7; color: #50575e; display: block; - line-height: 2.54545455; /* 28px */ + line-height: 2.72727273; /* 30px */ padding: 0 6px; text-align: center; } @@ -76,8 +76,8 @@ .wp-customizer .wp-picker-input-wrap .button.wp-picker-clear { margin-left: 6px; padding: 0 8px; - line-height: 2.54545455; /* 28px */ - min-height: 30px; + line-height: 2.72727273; /* 30px */ + min-height: 32px; } .wp-picker-container .iris-square-slider .ui-slider-handle:focus { @@ -97,7 +97,7 @@ margin: 0; padding: 0 5px; vertical-align: top; - min-height: 30px; + min-height: 32px; } .wp-color-picker::-webkit-input-placeholder { From c1eaccd51ccfe7ad420c075781d87ab7b464b87e Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Fri, 3 Apr 2026 13:32:34 +0000 Subject: [PATCH 11/57] =?UTF-8?q?Admin=20Reskin:=20Correct=20=E2=80=9DCopi?= =?UTF-8?q?ed!=E2=80=9D=20text=20alignment=20on=20Privacy=20Policy=20Guide?= =?UTF-8?q?=20screen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to [61645]. Reviewed by wildworks, SergeyBiryukov. Merges [62196] to the 7.0 branch. Props mukesh27, wildworks, audrasjb, shailu25, anupkankale, kapilpaul, SergeyBiryukov. Fixes #65009. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62203 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/edit.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index f2ff6a485767a..b98dd889c59fe 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -994,15 +994,16 @@ form#tags-filter { } .privacy-settings-accordion-actions { - text-align: right; - display: block; + justify-content: right; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 1em; } .privacy-settings-accordion-actions .success { display: none; color: #007017; - padding-right: 1em; - padding-top: 6px; } .privacy-settings-accordion-actions .success.visible { From b4ec3801558a9cb42bfcaff3456e200791795761 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Fri, 3 Apr 2026 20:22:46 +0000 Subject: [PATCH 12/57] Media: Update upload file overlay colors. Update the colors used for the file upload overlay mask to use the new admin theme colors. Reviewed by wildworks. Merges 62199 to the 7.0 branch. Props opurockey, huzaifaalmesbah, wildworks, audrasjb, manhar, joedolson. Fixes #65001. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62204 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/css/media-views.css | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css index 1b3c6edd7678f..f78a946c260f7 100644 --- a/src/wp-includes/css/media-views.css +++ b/src/wp-includes/css/media-views.css @@ -56,7 +56,7 @@ .media-frame a:focus { border-radius: 2px; box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); - color: #043959; + color: var(--wp-admin-theme-color-darker-20, #183ad6); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -244,13 +244,13 @@ .media-modal-close:hover, .media-modal-close:active { - color: #135e96; + color: var(--wp-admin-theme-color, #3858e9); } .media-modal-close:focus { - color: #135e96; - border-color: #4f94d4; - box-shadow: 0 0 3px rgba(34, 113, 177, 0.8); + color: var(--wp-admin-theme-color, #3858e9); + border-color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 3px rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.8); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -673,7 +673,7 @@ font-size: 14px; line-height: 1.28571428; background: transparent; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); text-align: left; text-decoration: none; cursor: pointer; @@ -684,7 +684,7 @@ } .media-menu .media-menu-item:active { - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); outline: none; } @@ -696,7 +696,7 @@ .media-menu .media-menu-item:focus { box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); - color: #043959; + color: var(--wp-admin-theme-color-darker-20, #183ad6); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -739,7 +739,7 @@ .media-router .media-menu-item:hover, .media-router .media-menu-item:active { - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); } .media-router .active, @@ -749,7 +749,7 @@ .media-router .media-menu-item:focus { box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); - color: #043959; + color: var(--wp-admin-theme-color-darker-20, #183ad6); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; z-index: 1; @@ -1321,8 +1321,8 @@ } .uploader-inline .close:focus { - outline: 1px solid #4f94d4; - box-shadow: 0 0 3px rgba(34, 113, 177, 0.8); + outline: 1px solid var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 3px rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.8); } .attachments-browser.hide-sidebar .attachments, @@ -1409,7 +1409,7 @@ height: 10px; min-width: 20px; width: 0; - background: #2271b1; + background: var(--wp-admin-theme-color, #3858e9); border-radius: 10px; transition: width 300ms; } @@ -1527,7 +1527,7 @@ .uploader-window, .wp-editor-wrap .uploader-editor.droppable { - background: rgba(10, 75, 120, 0.9); + background-color: rgba(var(--wp-admin-theme-color--rgb, 56, 88, 233), 0.9); } .uploader-window-content, @@ -1688,13 +1688,13 @@ margin: 1px 8px 1px -8px; line-height: 1.4; border-right: 1px solid #dcdcde; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); text-decoration: none; } .media-selection .button-link:hover, .media-selection .button-link:focus { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20, #183ad6); } .media-selection .button-link:last-child { @@ -1752,7 +1752,7 @@ .wp-core-ui .media-selection .attachment.details:focus { box-shadow: 0 0 0 1px #fff, - 0 0 2px 3px #4f94d4; + 0 0 2px 3px var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -1764,7 +1764,7 @@ .wp-core-ui .media-selection .attachment.details { box-shadow: 0 0 0 1px #fff, - 0 0 0 3px #2271b1; + 0 0 0 3px var(--wp-admin-theme-color, #3858e9); } .media-selection:after { @@ -2044,7 +2044,7 @@ margin: 0; padding: 0; background: transparent; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); font-size: 20px; line-height: 1; cursor: pointer; @@ -2053,9 +2053,9 @@ } .wp-core-ui.media-modal .image-editor .imgedit-help-toggle:focus { - color: #2271b1; - border-color: #2271b1; - box-shadow: 0 0 0 1px #2271b1; + color: var(--wp-admin-theme-color, #3858e9); + border-color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } From caf5f904d204c75024ea74ad7817e7ca11ae28c4 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Mon, 6 Apr 2026 20:02:52 +0000 Subject: [PATCH 13/57] Admin: Limit scope of admin notice link design. The design changes to admin notices links in the admin refresh were applied broadly to .notice, .error, and .updated classes, but these classes are sometimes used outside the context of an admin notice. Change selectors from .notice a, .error a, .updated a to div.notice a, div.error a, div.updated a. Reviewed by audrasjb. Merges [62200] to the 7.0 branch. Props opurockey, audrasjb, vgnavada, gaisma22, shailu25, rbcorrales, joedolson. Fixes #64976. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62211 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index c691383019f6d..28b881d363c7e 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1473,22 +1473,22 @@ div.error p, color: #1e1e1e; } -.notice a, -.error a, -.updated a { +div.notice a, +div.error a, +div.updated a { color: var(--wp-admin-theme-color-darker-10); text-decoration: underline; } -.notice a:hover, -.error a:hover, -.updated a:hover { +div.notice a:hover, +div.error a:hover, +div.updated a:hover { color: var(--wp-admin-theme-color-darker-20); } -.notice a:focus, -.error a:focus, -.updated a:focus { +div.notice a:focus, +div.error a:focus, +div.updated a:focus { box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); outline: 2px solid transparent; border-radius: 2px; From c254f40a87ec5d6e5d06cb75fdb6889b314ef594 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 6 Apr 2026 21:45:57 +0000 Subject: [PATCH 14/57] Editor: Bump pinned hash for the Gutenberg repository. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This updates the pinned hash from the `gutenberg` from `0d133bf7e7437d65d68a06551f3d613a7d8e4361` to `e2970ba736edb99e08fb369d4fb0c378189468ee`. The following changes are included: - WordPress/gutenberg#76478 Boot: Fix black area below content when sidebar is taller than page c… (WordPress/gutenberg#76764) - Style Book: Fix missing styles for classic themes in stylebook route (WordPress/gutenberg#76843) - RTC: Fix stuck "Join" link in post list when lock expires (WordPress/gutenberg#76795) - Icon: Fix center alignment in the editor for classic themes (WordPress/gutenberg#76878) - RTC: Fix notes not syncing between collaborative editors (WordPress/gutenberg#76873) - Latest Comments: Fix v1 deprecated block missing supports (WordPress/gutenberg#76877) - Connectors: Add Akismet as a default connector (WordPress/gutenberg#76828) - Restore with compaction update (WordPress/gutenberg#76872) - Improve JSDoc for abilities API (WordPress/gutenberg#76824) - Connectors: Replace plugin.slug with plugin.file (WordPress/gutenberg#76909) - Block visibility badge: use canvas iframe for viewport detection (WordPress/gutenberg#76889) - Connectors: Update help text from 'reset' to 'manage' (WordPress/gutenberg#76963) - Connectors: Hide Akismet unless already installed (WordPress/gutenberg#76962) - Wrap sync update processing in try/catch (WordPress/gutenberg#76968) - Backport: Improve validation and permission checks for `WP_HTTP_Polling_Sync_Server` (WordPress/gutenberg#76987) - Connectors: account for mu-plugins when resolving plugin.file status (WordPress/gutenberg#76994) A full list of changes can be found on GitHub: https://github.com/WordPress/gutenberg/compare/0d133bf7e7437d65d68a06551f3d613a7d8e4361…e2970ba736edb99e08fb369d4fb0c378189468ee. Log created with: git log --reverse --format="- %s" 0d133bf7e7437d65d68a06551f3d613a7d8e4361..e2970ba736edb99e08fb369d4fb0c378189468ee | sed 's|#\([0-9][0-9]*\)|https://github.com/WordPress/gutenberg/pull/\1|g; /github\.com\/WordPress\/gutenberg\/pull/!d' | pbcopy See #64595. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62212 602fd350-edb4-49c9-b593-d223f7449a82 --- package.json | 2 +- .../assets/script-loader-packages.php | 10 ++-- .../assets/script-modules-packages.php | 4 +- .../build/routes/connectors-home/content.js | 56 +++++++++++++++---- .../connectors-home/content.min.asset.php | 2 +- .../routes/connectors-home/content.min.js | 2 +- 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index bc9ddd279488f..731ab3c87597c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "0d133bf7e7437d65d68a06551f3d613a7d8e4361", + "sha": "e2970ba736edb99e08fb369d4fb0c378189468ee", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { diff --git a/src/wp-includes/assets/script-loader-packages.php b/src/wp-includes/assets/script-loader-packages.php index 04eef1a8a00f5..10af74b63ce36 100644 --- a/src/wp-includes/assets/script-loader-packages.php +++ b/src/wp-includes/assets/script-loader-packages.php @@ -100,7 +100,7 @@ 'wp-url', 'wp-warning' ), - 'version' => '0c1dfcebf759791c9a8b' + 'version' => '2300d40abe29e438beda' ), 'block-library.js' => array( 'dependencies' => array( @@ -142,7 +142,7 @@ 'import' => 'dynamic' ) ), - 'version' => 'd72ed53f961f90f21ed4' + 'version' => '67d1a681ec0100a25d78' ), 'block-serialization-default-parser.js' => array( 'dependencies' => array( @@ -428,7 +428,7 @@ 'import' => 'static' ) ), - 'version' => 'a688ac97344ffdfcca99' + 'version' => 'd36eb0c37b644e4cd4c8' ), 'edit-widgets.js' => array( 'dependencies' => array( @@ -519,7 +519,7 @@ 'import' => 'static' ) ), - 'version' => '49ff59c135229f1cc371' + 'version' => '63782008412a6163c9f0' ), 'element.js' => array( 'dependencies' => array( @@ -817,7 +817,7 @@ 'wp-hooks', 'wp-private-apis' ), - 'version' => '89ec294039260fd01952' + 'version' => '8186bfbc15b827d261f5' ), 'theme.js' => array( 'dependencies' => array( diff --git a/src/wp-includes/assets/script-modules-packages.php b/src/wp-includes/assets/script-modules-packages.php index d035354c60036..534ce123add0f 100644 --- a/src/wp-includes/assets/script-modules-packages.php +++ b/src/wp-includes/assets/script-modules-packages.php @@ -166,7 +166,7 @@ 'import' => 'static' ) ), - 'version' => '105defe2f1526f8a43e8' + 'version' => '42d3f09bba14cce3054d' ), 'connectors/index.js' => array( 'dependencies' => array( @@ -177,7 +177,7 @@ 'wp-i18n', 'wp-private-apis' ), - 'version' => 'e973aa806299e3d70144' + 'version' => '274797868955a828dfdc' ), 'core-abilities/index.js' => array( 'dependencies' => array( diff --git a/src/wp-includes/build/routes/connectors-home/content.js b/src/wp-includes/build/routes/connectors-home/content.js index f71de0935092c..c285e273ea082 100644 --- a/src/wp-includes/build/routes/connectors-home/content.js +++ b/src/wp-includes/build/routes/connectors-home/content.js @@ -702,7 +702,7 @@ var import_element4 = __toESM(require_element()); var import_i18n = __toESM(require_i18n()); import { speak } from "@wordpress/a11y"; function useConnectorPlugin({ - pluginSlug, + file: pluginFileFromServer, settingName, connectorName, isInstalled, @@ -714,6 +714,8 @@ function useConnectorPlugin({ const [isBusy, setIsBusy] = (0, import_element4.useState)(false); const [connectedState, setConnectedState] = (0, import_element4.useState)(initialIsConnected); const [pluginStatusOverride, setPluginStatusOverride] = (0, import_element4.useState)(null); + const pluginBasename = pluginFileFromServer?.replace(/\.php$/, ""); + const pluginSlug = pluginBasename?.includes("/") ? pluginBasename.split("/")[0] : pluginBasename; const { derivedPluginStatus, canManagePlugins, @@ -728,7 +730,7 @@ function useConnectorPlugin({ kind: "root", name: "plugin" }); - if (!pluginSlug) { + if (!pluginFileFromServer) { const hasLoaded = store2.hasFinishedResolution( "getEntityRecord", ["root", "site"] @@ -740,15 +742,14 @@ function useConnectorPlugin({ canInstallPlugins: canCreate }; } - const pluginId = `${pluginSlug}/plugin`; const plugin = store2.getEntityRecord( "root", "plugin", - pluginId + pluginBasename ); const hasFinished = store2.hasFinishedResolution( "getEntityRecord", - ["root", "plugin", pluginId] + ["root", "plugin", pluginBasename] ); if (!hasFinished) { return { @@ -779,7 +780,7 @@ function useConnectorPlugin({ canInstallPlugins: canCreate }; }, - [pluginSlug, settingName, isInstalled, isActivated] + [pluginBasename, settingName, isInstalled, isActivated] ); const pluginStatus = pluginStatusOverride ?? derivedPluginStatus; const canActivatePlugins = canManagePlugins; @@ -823,7 +824,7 @@ function useConnectorPlugin({ } }; const activatePlugin = async () => { - if (!pluginSlug) { + if (!pluginFileFromServer) { return; } setIsBusy(true); @@ -831,7 +832,10 @@ function useConnectorPlugin({ await saveEntityRecord( "root", "plugin", - { plugin: `${pluginSlug}/plugin`, status: "active" }, + { + plugin: pluginBasename, + status: "active" + }, { throwOnError: true } ); setPluginStatusOverride("active"); @@ -1030,6 +1034,27 @@ var DefaultConnectorLogo = () => /* @__PURE__ */ React.createElement( } ) ); +var AkismetLogo = () => /* @__PURE__ */ React.createElement( + "svg", + { + width: "40", + height: "40", + viewBox: "0 0 44 44", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + "aria-hidden": "true" + }, + /* @__PURE__ */ React.createElement("rect", { width: "44", height: "44", fill: "#357B49", rx: "6" }), + /* @__PURE__ */ React.createElement( + "path", + { + fill: "#fff", + fillRule: "evenodd", + d: "m29.746 28.31-6.392-16.797c-.152-.397-.305-.672-.789-.675-.673 0-1.408.611-1.746 1.316l-7.378 16.154c-.072.16-.143.311-.214.454-.5.995-1.045 1.546-2.357 1.626a.399.399 0 0 0-.16.033l-.01.004a.399.399 0 0 0-.23.392v.01c0 .054.01.106.03.155l.004.01a.416.416 0 0 0 .394.252h6.212a.417.417 0 0 0 .307-.12.416.416 0 0 0 .124-.305.398.398 0 0 0-.105-.302.399.399 0 0 0-.294-.127c-.757 0-2.197-.062-2.197-1.164.02-.318.103-.63.245-.916l1.399-3.152c.52-1.163 1.654-1.163 2.572-1.163h5.843c.023 0 .044 0 .062.003.13.014.16.081.214.242l1.534 4.07a2.857 2.857 0 0 1 .216 1.04c0 .054-.003.104-.01.153-.09.726-.831.887-1.49.887a.4.4 0 0 0-.294.127l-.007.008-.007.008a.401.401 0 0 0-.092.286v.01c0 .054.01.106.03.155l.005.01a.42.42 0 0 0 .395.252h7.011a.413.413 0 0 0 .279-.13.412.412 0 0 0 .11-.297.387.387 0 0 0-.09-.294.388.388 0 0 0-.277-.135c-1.448-.122-2.295-.643-2.847-2.08Zm-11.985-5.844 2.847-6.304c.361-.728.659-1.486.889-2.265 0-.06.03-.092.06-.092s.061.032.061.091c.02.122.045.247.073.374.197.888.584 1.878.914 2.723l.176.453 1.684 4.529a.927.927 0 0 1 .092.4.473.473 0 0 1-.009.094c-.041.202-.228.272-.602.272h-6.063c-.122 0-.184-.03-.184-.092a.36.36 0 0 1 .062-.183Zm17.107-.721c0 .786-.446 1.231-1.25 1.231-.806 0-1.125-.409-1.125-1.034 0-.786.465-1.231 1.25-1.231.785 0 1.125.427 1.125 1.034ZM9.629 23.002c.803 0 1.25-.447 1.25-1.231 0-.607-.343-1.036-1.128-1.036-.785 0-1.25.447-1.25 1.231 0 .625.325 1.036 1.128 1.036Z", + clipRule: "evenodd" + } + ) +); var GeminiLogo = () => /* @__PURE__ */ React.createElement( "svg", { @@ -1123,7 +1148,8 @@ function getConnectorData() { var CONNECTOR_LOGOS = { google: GeminiLogo, openai: OpenAILogo, - anthropic: ClaudeLogo + anthropic: ClaudeLogo, + akismet: AkismetLogo }; function getConnectorLogo(connectorId, logoUrl) { if (logoUrl) { @@ -1161,7 +1187,8 @@ function ApiKeyConnector({ const auth = authentication?.method === "api_key" ? authentication : void 0; const settingName = auth?.settingName ?? ""; const helpUrl = auth?.credentialsUrl ?? void 0; - const pluginSlug = plugin?.slug; + const pluginFile = plugin?.file?.replace(/\.php$/, ""); + const pluginSlug = pluginFile?.includes("/") ? pluginFile.split("/")[0] : pluginFile; let helpLabel; try { if (helpUrl) { @@ -1184,7 +1211,7 @@ function ApiKeyConnector({ saveApiKey, removeApiKey } = useConnectorPlugin({ - pluginSlug, + file: plugin?.file, settingName, connectorName: name, isInstalled: plugin?.isInstalled, @@ -1259,16 +1286,20 @@ function registerDefaultConnectors() { const connectors = getConnectorData(); const sanitize = (s) => s.replace(/[^a-z0-9-_]/gi, "-"); for (const [connectorId, data] of Object.entries(connectors)) { + if (connectorId === "akismet" && !data.plugin?.isInstalled) { + continue; + } const { authentication } = data; const connectorName = sanitize(connectorId); const args = { name: data.name, description: data.description, + type: data.type, logo: getConnectorLogo(connectorId, data.logoUrl), authentication, plugin: data.plugin }; - if (data.type === "ai_provider" && authentication.method === "api_key") { + if (authentication.method === "api_key") { args.render = ApiKeyConnector; } registerConnector(connectorName, args); @@ -1562,6 +1593,7 @@ function ConnectorsPage() { slug: connector.slug, name: connector.name, description: connector.description, + type: connector.type, logo: connector.logo, authentication: connector.authentication, plugin: connector.plugin diff --git a/src/wp-includes/build/routes/connectors-home/content.min.asset.php b/src/wp-includes/build/routes/connectors-home/content.min.asset.php index 9ef1fdf96351c..ef57aa56cd29b 100644 --- a/src/wp-includes/build/routes/connectors-home/content.min.asset.php +++ b/src/wp-includes/build/routes/connectors-home/content.min.asset.php @@ -1 +1 @@ - array('react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/connectors', 'import' => 'static'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => 'e598f70e4e13735c7300'); \ No newline at end of file + array('react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/connectors', 'import' => 'static'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => '067df442b07dc9245aee'); \ No newline at end of file diff --git a/src/wp-includes/build/routes/connectors-home/content.min.js b/src/wp-includes/build/routes/connectors-home/content.min.js index 1ea2ff593417f..ffe9257b61415 100644 --- a/src/wp-includes/build/routes/connectors-home/content.min.js +++ b/src/wp-includes/build/routes/connectors-home/content.min.js @@ -1 +1 @@ -var qt=Object.create;var qe=Object.defineProperty;var Tt=Object.getOwnPropertyDescriptor;var Vt=Object.getOwnPropertyNames;var Nt=Object.getPrototypeOf,Xt=Object.prototype.hasOwnProperty;var z=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Yt=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of Vt(t))!Xt.call(e,r)&&r!==n&&qe(e,r,{get:()=>t[r],enumerable:!(o=Tt(t,r))||o.enumerable});return e};var s=(e,t,n)=>(n=e!=null?qt(Nt(e)):{},Yt(t||!e||!e.__esModule?qe(n,"default",{value:e,enumerable:!0}):n,e));var I=z((bn,Te)=>{Te.exports=window.wp.i18n});var k=z((wn,Ve)=>{Ve.exports=window.wp.components});var ne=z((Ln,Ne)=>{Ne.exports=window.ReactJSXRuntime});var j=z((xn,Ye)=>{Ye.exports=window.wp.element});var C=z((Mn,Ae)=>{Ae.exports=window.React});var st=z((ir,it)=>{it.exports=window.wp.privateApis});var ae=z((yr,gt)=>{gt.exports=window.wp.data});var ie=z((xr,mt)=>{mt.exports=window.wp.coreData});var ht=z((Gr,vt)=>{vt.exports=window.wp.url});function Xe(e){var t,n,o="";if(typeof e=="string"||typeof e=="number")o+=e;else if(typeof e=="object")if(Array.isArray(e)){var r=e.length;for(t=0;t(0,Ce.jsx)(o,{ref:a,className:S("admin-ui-navigable-region",t),"aria-label":n,role:"region",tabIndex:"-1",...r,children:e}));Ze.displayName="NavigableRegion";var Ee=Ze;var Ke=s(C(),1),We={};function pe(e,t){let n=Ke.useRef(We);return n.current===We&&(n.current=e(t)),n}function ge(e,...t){let n=new URL(`https://base-ui.com/production-error/${e}`);return t.forEach(o=>n.searchParams.append("args[]",o)),`Base UI error #${e}; visit ${n} for the full message.`}var re=s(C(),1);function me(e,t,n,o){let r=pe(ke).current;return Ct(r,e,t,n,o)&&Ue(r,[e,t,n,o]),r.callback}function Ie(e){let t=pe(ke).current;return Zt(t,e)&&Ue(t,e),t.callback}function ke(){return{callback:null,cleanup:null,refs:[]}}function Ct(e,t,n,o,r){return e.refs[0]!==t||e.refs[1]!==n||e.refs[2]!==o||e.refs[3]!==r}function Zt(e,t){return e.refs.length!==t.length||e.refs.some((n,o)=>n!==t[o])}function Ue(e,t){if(e.refs=t,t.every(n=>n==null)){e.callback=null;return}e.callback=n=>{if(e.cleanup&&(e.cleanup(),e.cleanup=null),n!=null){let o=Array(t.length).fill(null);for(let r=0;r{for(let r=0;r=e}function ve(e){if(!Fe.isValidElement(e))return null;let t=e,n=t.props;return(Je(19)?n?.ref:t.ref)??null}function U(e,t){if(e&&!t)return e;if(!e&&t)return t;if(e||t)return{...e,...t}}function _e(e,t){let n={};for(let o in e){let r=e[o];if(t?.hasOwnProperty(o)){let a=t[o](r);a!=null&&Object.assign(n,a);continue}r===!0?n[`data-${o.toLowerCase()}`]="":r&&(n[`data-${o.toLowerCase()}`]=r.toString())}return n}function $e(e,t){return typeof e=="function"?e(t):e}function et(e,t){return typeof e=="function"?e(t):e}var J={};function Z(e,t,n,o,r){let a={...he(e,J)};return t&&(a=Q(a,t)),n&&(a=Q(a,n)),o&&(a=Q(a,o)),r&&(a=Q(a,r)),a}function tt(e){if(e.length===0)return J;if(e.length===1)return he(e[0],J);let t={...he(e[0],J)};for(let n=1;n=65&&r<=90&&(typeof t=="function"||typeof t>"u")}function nt(e){return typeof e=="function"}function he(e,t){return nt(e)?e(t):e??J}function Kt(e,t){return t?e?n=>{if(kt(n)){let r=n;It(r);let a=t(r);return r.baseUIHandlerPrevented||e?.(r),a}let o=t(n);return e?.(n),o}:t:e}function It(e){return e.preventBaseUIHandler=()=>{e.baseUIHandlerPrevented=!0},e}function Pe(e,t){return t?e?t+" "+e:t:e}function kt(e){return e!=null&&typeof e=="object"&&"nativeEvent"in e}var Ut=Object.freeze([]),B=Object.freeze({});var be=s(C(),1);function rt(e,t,n={}){let o=t.render,r=Qt(t,n);if(n.enabled===!1)return null;let a=n.state??B;return Jt(e,o,r,a)}function Qt(e,t={}){let{className:n,style:o,render:r}=e,{state:a=B,ref:i,props:l,stateAttributesMapping:p,enabled:u=!0}=t,d=u?$e(n,a):void 0,M=u?et(o,a):void 0,O=u?_e(a,p):B,f=u?U(O,Array.isArray(l)?tt(l):l)??B:B;return typeof document<"u"&&(u?Array.isArray(i)?f.ref=Ie([f.ref,ve(r),...i]):f.ref=me(f.ref,ve(r),i):me(null,null)),u?(d!==void 0&&(f.className=Pe(f.className,d)),M!==void 0&&(f.style=U(f.style,M)),f):B}function Jt(e,t,n,o){if(t){if(typeof t=="function")return t(n,o);let r=Z(n,t.props);return r.ref=n.ref,re.cloneElement(t,r)}if(e&&typeof e=="string")return Ft(e,n);throw new Error(ge(8))}function Ft(e,t){return e==="button"?(0,be.createElement)("button",{type:"button",...t,key:t.key}):e==="img"?(0,be.createElement)("img",{alt:"",...t,key:t.key}):re.createElement(e,t)}function oe(e){return rt(e.defaultTagName??"div",e,e)}var at=s(j(),1);if(typeof document<"u"&&!document.head.querySelector("style[data-wp-hash='244b5c59c0']")){let e=document.createElement("style");e.setAttribute("data-wp-hash","244b5c59c0"),e.appendChild(document.createTextNode('@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;@layer wp-ui-components{._96e6251aad1a6136__badge{border-radius:var(--wpds-border-radius-lg,8px);font-family:var(--wpds-font-family-body,-apple-system,system-ui,"Segoe UI","Roboto","Oxygen-Sans","Ubuntu","Cantarell","Helvetica Neue",sans-serif);font-size:var(--wpds-font-size-sm,12px);font-weight:var(--wpds-font-weight-regular,400);line-height:var(--wpds-font-line-height-xs,16px);padding-block:var(--wpds-dimension-padding-xs,4px);padding-inline:var(--wpds-dimension-padding-sm,8px)}._99f7158cb520f750__is-high-intent{background-color:var(--wpds-color-bg-surface-error,#f6e6e3);color:var(--wpds-color-fg-content-error,#470000)}.c20ebef2365bc8b7__is-medium-intent{background-color:var(--wpds-color-bg-surface-warning,#fde6bd);color:var(--wpds-color-fg-content-warning,#2e1900)}._365e1626c6202e52__is-low-intent{background-color:var(--wpds-color-bg-surface-caution,#fee994);color:var(--wpds-color-fg-content-caution,#281d00)}._33f8198127ddf4ef__is-stable-intent{background-color:var(--wpds-color-bg-surface-success,#c5f7cc);color:var(--wpds-color-fg-content-success,#002900)}._04c1aca8fc449412__is-informational-intent{background-color:var(--wpds-color-bg-surface-info,#deebfa);color:var(--wpds-color-fg-content-info,#001b4f)}._90726e69d495ec19__is-draft-intent{background-color:var(--wpds-color-bg-surface-neutral-weak,#f0f0f0);color:var(--wpds-color-fg-content-neutral,#1e1e1e)}._898f4a544993bd39__is-none-intent{background-color:var(--wpds-color-bg-surface-neutral,#f8f8f8);color:var(--wpds-color-fg-content-neutral-weak,#6d6d6d)}}')),document.head.appendChild(e)}var ot={badge:"_96e6251aad1a6136__badge","is-high-intent":"_99f7158cb520f750__is-high-intent","is-medium-intent":"c20ebef2365bc8b7__is-medium-intent","is-low-intent":"_365e1626c6202e52__is-low-intent","is-stable-intent":"_33f8198127ddf4ef__is-stable-intent","is-informational-intent":"_04c1aca8fc449412__is-informational-intent","is-draft-intent":"_90726e69d495ec19__is-draft-intent","is-none-intent":"_898f4a544993bd39__is-none-intent"},we=(0,at.forwardRef)(function({children:t,intent:n="none",render:o,className:r,...a},i){return oe({render:o,defaultTagName:"span",ref:i,props:Z(a,{className:S(ot.badge,ot[`is-${n}-intent`],r),children:t})})});var ct=s(j(),1);if(typeof document<"u"&&!document.head.querySelector("style[data-wp-hash='71d20935c2']")){let e=document.createElement("style");e.setAttribute("data-wp-hash","71d20935c2"),e.appendChild(document.createTextNode("@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;@layer wp-ui-components{._19ce0419607e1896__stack{display:flex}}")),document.head.appendChild(e)}var _t={stack:"_19ce0419607e1896__stack"},$t={xs:"var(--wpds-dimension-gap-xs, 4px)",sm:"var(--wpds-dimension-gap-sm, 8px)",md:"var(--wpds-dimension-gap-md, 12px)",lg:"var(--wpds-dimension-gap-lg, 16px)",xl:"var(--wpds-dimension-gap-xl, 24px)","2xl":"var(--wpds-dimension-gap-2xl, 32px)","3xl":"var(--wpds-dimension-gap-3xl, 40px)"},E=(0,ct.forwardRef)(function({direction:t,gap:n,align:o,justify:r,wrap:a,render:i,...l},p){let u={gap:n&&$t[n],alignItems:o,justifyContent:r,flexDirection:t,flexWrap:a};return oe({render:i,ref:p,props:Z(l,{style:u,className:_t.stack})})});var lt=s(k(),1),{Fill:dt,Slot:ut}=(0,lt.createSlotFill)("SidebarToggle");var P=s(ne(),1);function ft({headingLevel:e=2,breadcrumbs:t,badges:n,title:o,subTitle:r,actions:a,showSidebarToggle:i=!0}){let l=`h${e}`;return(0,P.jsxs)(E,{direction:"column",className:"admin-ui-page__header",render:(0,P.jsx)("header",{}),children:[(0,P.jsxs)(E,{direction:"row",justify:"space-between",gap:"sm",children:[(0,P.jsxs)(E,{direction:"row",gap:"sm",align:"center",justify:"start",children:[i&&(0,P.jsx)(ut,{bubblesVirtually:!0,className:"admin-ui-page__sidebar-toggle-slot"}),o&&(0,P.jsx)(l,{className:"admin-ui-page__header-title",children:o}),t,n]}),(0,P.jsx)(E,{direction:"row",gap:"sm",style:{width:"auto",flexShrink:0},className:"admin-ui-page__header-actions",align:"center",children:a})]}),r&&(0,P.jsx)("p",{className:"admin-ui-page__header-subtitle",children:r})]})}var F=s(ne(),1);function pt({headingLevel:e,breadcrumbs:t,badges:n,title:o,subTitle:r,children:a,className:i,actions:l,hasPadding:p=!1,showSidebarToggle:u=!0}){let d=S("admin-ui-page",i);return(0,F.jsxs)(Ee,{className:d,ariaLabel:o,children:[(o||t||n)&&(0,F.jsx)(ft,{headingLevel:e,breadcrumbs:t,badges:n,title:o,subTitle:r,actions:l,showSidebarToggle:u}),p?(0,F.jsx)("div",{className:"admin-ui-page__content has-padding",children:a}):a]})}pt.SidebarToggleFill=dt;var Le=pt;var w=s(k()),Bt=s(ae()),Ht=s(j()),N=s(I()),Rt=s(ie());import{privateApis as un}from"@wordpress/connectors";if(typeof document<"u"&&!document.head.querySelector("style[data-wp-hash='1b00f16b8d']")){let e=document.createElement("style");e.setAttribute("data-wp-hash","1b00f16b8d"),e.appendChild(document.createTextNode(".connectors-page{box-sizing:border-box;margin:0 auto;max-width:680px;padding:24px;width:100%}.connectors-page .components-item{background:#fff;border:1px solid #ddd;border-radius:8px;overflow:hidden;padding:20px;scroll-margin-top:120px}.connectors-page .connector-settings__error{color:#cc1818}.connectors-page .connector-settings .components-text-control__input{font-family:monospace;scroll-margin-top:120px}.connectors-page--empty{align-items:center;display:flex;flex-direction:column;flex-grow:1;gap:32px;justify-content:center;text-align:center}.connectors-page .ai-plugin-callout{background:linear-gradient(90deg,#fff9,#fff9),linear-gradient(90deg,#89dcdc,#c7eb5c 46.15%,#a920c1);border-radius:8px;overflow:hidden;padding:24px;padding-inline-end:220px;position:relative}[dir=rtl] .connectors-page .ai-plugin-callout{background:linear-gradient(270deg,#fff9,#fff9),linear-gradient(270deg,#89dcdc,#c7eb5c 46.15%,#a920c1)}.connectors-page .ai-plugin-callout__content{align-items:flex-start;display:flex;flex-direction:column;gap:12px;padding-top:2px}.connectors-page .ai-plugin-callout__content p{font-size:13px;line-height:20px;margin:0}.connectors-page .ai-plugin-callout__decoration{height:248px;inset-inline-end:8px;position:absolute;top:-15px;width:248px}.connectors-page>p{color:#949494;text-align:center}@media (max-width:680px){.connectors-page .ai-plugin-callout{padding:12px;padding-inline-end:84px}.connectors-page .ai-plugin-callout__decoration{height:134px;inset-inline-end:4px;top:-8px;width:134px}}@media (max-width:480px){.connectors-page{padding:8px}.connectors-page .components-item{padding:12px}.connectors-page .components-item>.components-v-stack>.components-h-stack:first-child svg{height:32px;width:32px}.connectors-page .components-item>.components-v-stack>.components-h-stack:first-child>.components-h-stack:last-child{align-items:flex-end;flex-direction:column}}")),document.head.appendChild(e)}var ee=s(k()),Me=s(ie()),de=s(ae()),b=s(j()),m=s(I()),Mt=s(ht());import{speak as le}from"@wordpress/a11y";var ce=s(k()),$=s(j()),xe=s(I());import{__experimentalRegisterConnector as en,__experimentalConnectorItem as tn,__experimentalDefaultConnectorSettings as nn}from"@wordpress/connectors";var ye=s(ie()),se=s(ae()),_=s(j()),c=s(I());import{speak as V}from"@wordpress/a11y";function Pt({pluginSlug:e,settingName:t,connectorName:n,isInstalled:o,isActivated:r,keySource:a="none",initialIsConnected:i=!1}){let[l,p]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[M,O]=(0,_.useState)(i),[f,X]=(0,_.useState)(null),{derivedPluginStatus:D,canManagePlugins:L,currentApiKey:y,canInstallPlugins:v}=(0,se.useSelect)(R=>{let q=R(ye.store),K=q.getEntityRecord("root","site")?.[t]??"",T=!!q.canUser("create",{kind:"root",name:"plugin"});if(!e)return{derivedPluginStatus:q.hasFinishedResolution("getEntityRecord",["root","site"])?"active":"checking",canManagePlugins:void 0,currentApiKey:K,canInstallPlugins:T};let He=`${e}/plugin`,Re=q.getEntityRecord("root","plugin",He);if(!q.hasFinishedResolution("getEntityRecord",["root","plugin",He]))return{derivedPluginStatus:"checking",canManagePlugins:void 0,currentApiKey:K,canInstallPlugins:T};if(Re)return{derivedPluginStatus:Re.status==="active"?"active":"inactive",canManagePlugins:!0,currentApiKey:K,canInstallPlugins:T};let fe="not-installed";return r?fe="active":o&&(fe="inactive"),{derivedPluginStatus:fe,canManagePlugins:!1,currentApiKey:K,canInstallPlugins:T}},[e,t,o,r]),g=f??D,x=L,Y=g==="active"&&M||f==="active"&&!!y,{saveEntityRecord:h,invalidateResolution:G}=(0,se.useDispatch)(ye.store),A=async()=>{if(e){d(!0);try{await h("root","plugin",{slug:e,status:"active"},{throwOnError:!0}),X("active"),G("getEntityRecord",["root","site"]),p(!0),V((0,c.sprintf)((0,c.__)("Plugin for %s installed and activated successfully."),n))}catch{V((0,c.sprintf)((0,c.__)("Failed to install plugin for %s."),n),"assertive")}finally{d(!1)}}},W=async()=>{if(e){d(!0);try{await h("root","plugin",{plugin:`${e}/plugin`,status:"active"},{throwOnError:!0}),X("active"),G("getEntityRecord",["root","site"]),p(!0),V((0,c.sprintf)((0,c.__)("Plugin for %s activated successfully."),n))}catch{V((0,c.sprintf)((0,c.__)("Failed to activate plugin for %s."),n),"assertive")}finally{d(!1)}}};return{pluginStatus:g,canInstallPlugins:v,canActivatePlugins:x,isExpanded:l,setIsExpanded:p,isBusy:u,isConnected:Y,currentApiKey:y,keySource:a,handleButtonClick:()=>{if(g==="not-installed"){if(v===!1)return;A()}else if(g==="inactive"){if(x===!1)return;W()}else p(!l)},getButtonLabel:()=>{if(u)return g==="not-installed"?(0,c.__)("Installing\u2026"):(0,c.__)("Activating\u2026");if(l)return(0,c.__)("Cancel");if(Y)return(0,c.__)("Edit");switch(g){case"checking":return(0,c.__)("Checking\u2026");case"not-installed":return(0,c.__)("Install");case"inactive":return(0,c.__)("Activate");case"active":return(0,c.__)("Set up")}},saveApiKey:async R=>{let q=y;try{let T=(await h("root","site",{[t]:R},{throwOnError:!0}))?.[t];if(R&&(T===q||!T))throw new Error("It was not possible to connect to the provider using this key.");O(!0),V((0,c.sprintf)((0,c.__)("%s connected successfully."),n))}catch(te){throw console.error("Failed to save API key:",te),te}},removeApiKey:async()=>{try{await h("root","site",{[t]:""},{throwOnError:!0}),O(!1),V((0,c.sprintf)((0,c.__)("%s disconnected."),n))}catch(R){throw console.error("Failed to remove API key:",R),V((0,c.sprintf)((0,c.__)("Failed to disconnect %s."),n),"assertive"),R}}}}var bt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364l2.0201-1.1685a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.4043-.6813zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z",fill:"currentColor"})),wt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 32 32",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M6.2 21.024L12.416 17.536L12.52 17.232L12.416 17.064H12.112L11.072 17L7.52 16.904L4.44 16.776L1.456 16.616L0.704 16.456L0 15.528L0.072 15.064L0.704 14.64L1.608 14.72L3.608 14.856L6.608 15.064L8.784 15.192L12.008 15.528H12.52L12.592 15.32L12.416 15.192L12.28 15.064L9.176 12.96L5.816 10.736L4.056 9.456L3.104 8.808L2.624 8.2L2.416 6.872L3.28 5.92L4.44 6L4.736 6.08L5.912 6.984L8.424 8.928L11.704 11.344L12.184 11.744L12.376 11.608L12.4 11.512L12.184 11.152L10.4 7.928L8.496 4.648L7.648 3.288L7.424 2.472C7.344 2.136 7.288 1.856 7.288 1.512L8.272 0.176L8.816 0L10.128 0.176L10.68 0.656L11.496 2.52L12.816 5.456L14.864 9.448L15.464 10.632L15.784 11.728L15.904 12.064H16.112V11.872L16.28 9.624L16.592 6.864L16.896 3.312L17 2.312L17.496 1.112L18.48 0.464L19.248 0.832L19.88 1.736L19.792 2.32L19.416 4.76L18.68 8.584L18.2 11.144H18.48L18.8 10.824L20.096 9.104L22.272 6.384L23.232 5.304L24.352 4.112L25.072 3.544H26.432L27.432 5.032L26.984 6.568L25.584 8.344L24.424 9.848L22.76 12.088L21.72 13.88L21.816 14.024L22.064 14L25.824 13.2L27.856 12.832L30.28 12.416L31.376 12.928L31.496 13.448L31.064 14.512L28.472 15.152L25.432 15.76L20.904 16.832L20.848 16.872L20.912 16.952L22.952 17.144L23.824 17.192H25.96L29.936 17.488L30.976 18.176L31.6 19.016L31.496 19.656L29.896 20.472L27.736 19.96L22.696 18.76L20.968 18.328H20.728V18.472L22.168 19.88L24.808 22.264L28.112 25.336L28.28 26.096L27.856 26.696L27.408 26.632L24.504 24.448L23.384 23.464L20.848 21.328H20.68V21.552L21.264 22.408L24.352 27.048L24.512 28.472L24.288 28.936L23.488 29.216L22.608 29.056L20.8 26.52L18.936 23.664L17.432 21.104L17.248 21.208L16.36 30.768L15.944 31.256L14.984 31.624L14.184 31.016L13.76 30.032L14.184 28.088L14.696 25.552L15.112 23.536L15.488 21.032L15.712 20.2L15.696 20.144L15.512 20.168L13.624 22.76L10.752 26.64L8.48 29.072L7.936 29.288L6.992 28.8L7.08 27.928L7.608 27.152L10.752 23.152L12.648 20.672L13.872 19.24L13.864 19.032H13.792L5.44 24.456L3.952 24.648L3.312 24.048L3.392 23.064L3.696 22.744L6.208 21.016L6.2 21.024Z",fill:"#D97757"})),Lt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 32 32",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M0 4C0 1.79086 1.79086 0 4 0H28C30.2091 0 32 1.79086 32 4V28C32 30.2091 30.2091 32 28 32H4C1.79086 32 0 30.2091 0 28V4Z",fill:"#F0F0F0"}),React.createElement("path",{d:"M14.5 8V12H17.5V8H19V12H20.5C20.7652 12 21.0196 12.1054 21.2071 12.2929C21.3946 12.4804 21.5 12.7348 21.5 13V17L18.5 21V23C18.5 23.2652 18.3946 23.5196 18.2071 23.7071C18.0196 23.8946 17.7652 24 17.5 24H14.5C14.2348 24 13.9804 23.8946 13.7929 23.7071C13.6054 23.5196 13.5 23.2652 13.5 23V21L10.5 17V13C10.5 12.7348 10.6054 12.4804 10.7929 12.2929C10.9804 12.1054 11.2348 12 11.5 12H13V8H14.5ZM15 20.5V22.5H17V20.5L20 16.5V13.5H12V16.5L15 20.5Z",fill:"#949494"})),yt=()=>React.createElement("svg",{width:"40",height:"40",style:{flex:"none",lineHeight:1},viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"#3186FF"}),React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"url(#lobe-icons-gemini-fill-0)"}),React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"url(#lobe-icons-gemini-fill-1)"}),React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"url(#lobe-icons-gemini-fill-2)"}),React.createElement("defs",null,React.createElement("linearGradient",{gradientUnits:"userSpaceOnUse",id:"lobe-icons-gemini-fill-0",x1:"7",x2:"11",y1:"15.5",y2:"12"},React.createElement("stop",{stopColor:"#08B962"}),React.createElement("stop",{offset:"1",stopColor:"#08B962",stopOpacity:"0"})),React.createElement("linearGradient",{gradientUnits:"userSpaceOnUse",id:"lobe-icons-gemini-fill-1",x1:"8",x2:"11.5",y1:"5.5",y2:"11"},React.createElement("stop",{stopColor:"#F94543"}),React.createElement("stop",{offset:"1",stopColor:"#F94543",stopOpacity:"0"})),React.createElement("linearGradient",{gradientUnits:"userSpaceOnUse",id:"lobe-icons-gemini-fill-2",x1:"3.5",x2:"17.5",y1:"13.5",y2:"12"},React.createElement("stop",{stopColor:"#FABC12"}),React.createElement("stop",{offset:".46",stopColor:"#FABC12",stopOpacity:"0"}))));function Ge(){try{return JSON.parse(document.getElementById("wp-script-module-data-options-connectors-wp-admin")?.textContent??"")?.connectors??{}}catch{return{}}}var rn={google:yt,openai:bt,anthropic:wt};function on(e,t){if(t)return React.createElement("img",{src:t,alt:"",width:40,height:40});let n=rn[e];return React.createElement(n||Lt,null)}var an=()=>React.createElement("span",{style:{color:"#345b37",backgroundColor:"#eff8f0",padding:"4px 12px",borderRadius:"2px",fontSize:"13px",fontWeight:500,whiteSpace:"nowrap"}},(0,xe.__)("Connected")),sn=()=>React.createElement(we,null,(0,xe.__)("Not available"));function cn({name:e,description:t,logo:n,authentication:o,plugin:r}){let a=o?.method==="api_key"?o:void 0,i=a?.settingName??"",l=a?.credentialsUrl??void 0,p=r?.slug,u;try{l&&(u=new URL(l).hostname)}catch{}let{pluginStatus:d,canInstallPlugins:M,canActivatePlugins:O,isExpanded:f,setIsExpanded:X,isBusy:D,isConnected:L,currentApiKey:y,keySource:v,handleButtonClick:g,getButtonLabel:x,saveApiKey:Y,removeApiKey:h}=Pt({pluginSlug:p,settingName:i,connectorName:e,isInstalled:r?.isInstalled,isActivated:r?.isActivated,keySource:a?.keySource,initialIsConnected:a?.isConnected}),G=v==="env"||v==="constant",A=d==="not-installed"&&M===!1||d==="inactive"&&O===!1,W=!A,ue=(0,$.useRef)(null),H=(0,$.useRef)(!1);(0,$.useEffect)(()=>{H.current&&!D&&(H.current=!1,ue.current?.focus())},[D,f,L]);let je=()=>{(d==="not-installed"||d==="inactive")&&(H.current=!0),g()};return React.createElement(tn,{className:p?`connector-item--${p}`:void 0,logo:n,name:e,description:t,actionArea:React.createElement(ce.__experimentalHStack,{spacing:3,expanded:!1},L&&React.createElement(an,null),A&&React.createElement(sn,null),W&&React.createElement(ce.Button,{ref:ue,variant:f||L?"tertiary":"secondary",size:"compact",onClick:je,disabled:d==="checking"||D,isBusy:D},x()))},f&&d==="active"&&React.createElement(nn,{key:L?"connected":"setup",initialValue:G?"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022":y,helpUrl:l,helpLabel:u,readOnly:L||G,keySource:v,onRemove:G?void 0:async()=>{H.current=!0;try{await h()}catch{H.current=!1}},onSave:async Be=>{await Y(Be),H.current=!0,X(!1)}}))}function xt(){let e=Ge(),t=n=>n.replace(/[^a-z0-9-_]/gi,"-");for(let[n,o]of Object.entries(e)){let{authentication:r}=o,a=t(n),i={name:o.name,description:o.description,logo:on(n,o.logoUrl),authentication:r,plugin:o.plugin};o.type==="ai_provider"&&r.method==="api_key"&&(i.render=cn),en(a,i)}}function Gt(){return React.createElement("div",{className:"ai-plugin-callout__decoration","aria-hidden":"true"},React.createElement("svg",{viewBox:"0 0 248 248",xmlns:"http://www.w3.org/2000/svg",xmlnsXlink:"http://www.w3.org/1999/xlink",focusable:"false",style:{width:"100%",height:"100%"}},React.createElement("image",{href:"data:image/png;base64,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",width:"248",height:"248",style:{mixBlendMode:"multiply"}}),React.createElement("rect",{x:"184.055",y:"54.995",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"170.059",y:"44.06",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"200.238",y:"77.302",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"212.048",y:"87.8",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"206.799",y:"83.425",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"204.175",y:"85.612",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"219.046",y:"103.108",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"154.751",y:"30.064",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"188.866",y:"63.742",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"148.189",y:"34",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"134.051",y:"31.707",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"126.124",y:"24.771",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"115.385",y:"29.19",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"95.702",y:"31.376",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"91.766",y:"27.002",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"90.454",y:"32.688",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"184.389",y:"45.58",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"162.185",y:"41.873",width:"2.187",height:"2.187"})))}var zt="ai",ze="ai/ai",ln="https://wordpress.org/plugins/ai/",Oe=Object.values(Ge()),dn=Oe.some(e=>e.type==="ai_provider"),Ot=[];for(let e of Oe)e.type==="ai_provider"&&e.authentication.method==="api_key"&&Ot.push(e.authentication.settingName);function Dt(){let[e,t]=(0,b.useState)(!1),[n,o]=(0,b.useState)(!1),r=(0,b.useRef)(null);(0,b.useEffect)(()=>{n&&r.current?.focus()},[n]);let a=(0,b.useRef)(Oe.some(v=>v.type==="ai_provider"&&v.authentication.method==="api_key"&&v.authentication.isConnected)).current,{pluginStatus:i,canInstallPlugins:l,canManagePlugins:p,hasConnectedProvider:u}=(0,de.useSelect)(v=>{let g=v(Me.store),x=!!g.canUser("create",{kind:"root",name:"plugin"}),Y=g.getEntityRecord("root","site"),h=a||Ot.some(W=>!!Y?.[W]),G=g.getEntityRecord("root","plugin",ze);return g.hasFinishedResolution("getEntityRecord",["root","plugin",ze])?G?{pluginStatus:G.status==="active"?"active":"inactive",canInstallPlugins:x,canManagePlugins:!0,hasConnectedProvider:h}:{pluginStatus:"not-installed",canInstallPlugins:x,canManagePlugins:x,hasConnectedProvider:h}:{pluginStatus:"checking",canInstallPlugins:x,canManagePlugins:void 0,hasConnectedProvider:h}},[]),{saveEntityRecord:d}=(0,de.useDispatch)(Me.store),M=async()=>{t(!0);try{await d("root","plugin",{slug:zt,status:"active"},{throwOnError:!0}),o(!0),le((0,m.__)("AI plugin installed and activated successfully."))}catch{le((0,m.__)("Failed to install the AI plugin."),"assertive")}finally{t(!1)}},O=async()=>{t(!0);try{await d("root","plugin",{plugin:ze,status:"active"},{throwOnError:!0}),o(!0),le((0,m.__)("AI plugin activated successfully."))}catch{le((0,m.__)("Failed to activate the AI plugin."),"assertive")}finally{t(!1)}};if(!dn||i==="checking"||i==="active"&&a&&!n||i==="not-installed"&&l===!1||i==="inactive"&&p===!1)return null;let f=i==="active"&&!u,X=i==="active"&&u&&(!a||n),D=i==="not-installed"||i==="inactive",L=()=>X?(0,m.__)("The AI plugin is ready to use. You can use it to generate featured images, alt text, titles, excerpts and more. Learn more"):f?(0,m.__)("The AI plugin is installed. Connect a provider below to generate featured images, alt text, titles, excerpts, and more. Learn more"):(0,m.__)("The AI plugin can use your connectors to generate featured images, alt text, titles, excerpts and more. Learn more"),y=()=>i==="not-installed"?{label:e?(0,m.__)("Installing\u2026"):(0,m.__)("Install the AI plugin"),disabled:e,onClick:e?void 0:M}:{label:e?(0,m.__)("Activating\u2026"):(0,m.__)("Activate the AI plugin"),disabled:e,onClick:e?void 0:O};return React.createElement("div",{className:"ai-plugin-callout"},React.createElement("div",{className:"ai-plugin-callout__content"},React.createElement("p",null,(0,b.createInterpolateElement)(L(),{strong:React.createElement("strong",null),a:React.createElement(ee.ExternalLink,{href:ln})})),D?React.createElement(ee.Button,{variant:"primary",size:"compact",isBusy:e,disabled:y().disabled,accessibleWhenDisabled:!0,onClick:y().onClick},y().label):React.createElement(ee.Button,{ref:r,variant:"secondary",size:"compact",href:(0,Mt.addQueryArgs)("options-general.php",{page:zt})},(0,m.__)("Control features in the AI plugin"))),React.createElement(Gt,null))}var jt=s(st()),{lock:Yr,unlock:De}=(0,jt.__dangerousOptInToUnstableAPIsOnlyForCoreModules)("I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.","@wordpress/routes");var{store:fn}=De(un);xt();function pn(){let{connectors:e,canInstallPlugins:t}=(0,Bt.useSelect)(r=>({connectors:De(r(fn)).getConnectors(),canInstallPlugins:r(Rt.store).canUser("create",{kind:"root",name:"plugin"})}),[]),o=e.filter(r=>r.render).length===0;return React.createElement(Le,{title:(0,N.__)("Connectors"),headingLevel:1,subTitle:(0,N.__)("All of your API keys and credentials are stored here and shared across plugins. Configure once and use everywhere.")},React.createElement("div",{className:`connectors-page${o?" connectors-page--empty":""}`},o?React.createElement(w.__experimentalVStack,{alignment:"center",spacing:3,style:{maxWidth:480}},React.createElement(w.__experimentalVStack,{alignment:"center",spacing:2},React.createElement(w.__experimentalHeading,{level:2,size:15,weight:600},(0,N.__)("No connectors yet")),React.createElement(w.__experimentalText,{size:12},(0,N.__)("Connectors appear here when you install plugins that use external services. Each plugin registers the API keys it needs, and you manage them all in one place."))),React.createElement(w.Button,{variant:"secondary",href:"plugin-install.php"},(0,N.__)("Learn more"))):React.createElement(w.__experimentalVStack,{spacing:3},React.createElement(Dt,null),e.map(r=>r.render?React.createElement(r.render,{key:r.slug,slug:r.slug,name:r.name,description:r.description,logo:r.logo,authentication:r.authentication,plugin:r.plugin}):null)),t&&React.createElement("p",null,(0,Ht.createInterpolateElement)((0,N.__)("If the connector you need is not listed, search the plugin directory to see if a connector is available."),{a:React.createElement("a",{href:"plugin-install.php?s=connector&tab=search&type=tag"})}))))}function gn(){return React.createElement(pn,null)}var mn=gn;export{mn as stage}; +var Tt=Object.create;var qe=Object.defineProperty;var Vt=Object.getOwnPropertyDescriptor;var Nt=Object.getOwnPropertyNames;var Xt=Object.getPrototypeOf,Yt=Object.prototype.hasOwnProperty;var O=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var St=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of Nt(t))!Yt.call(e,r)&&r!==n&&qe(e,r,{get:()=>t[r],enumerable:!(o=Vt(t,r))||o.enumerable});return e};var s=(e,t,n)=>(n=e!=null?Tt(Xt(e)):{},St(t||!e||!e.__esModule?qe(n,"default",{value:e,enumerable:!0}):n,e));var I=O((Ln,Te)=>{Te.exports=window.wp.i18n});var k=O((yn,Ve)=>{Ve.exports=window.wp.components});var re=O((xn,Ne)=>{Ne.exports=window.ReactJSXRuntime});var H=O((zn,Ye)=>{Ye.exports=window.wp.element});var E=O((Dn,Ae)=>{Ae.exports=window.React});var st=O((lr,it)=>{it.exports=window.wp.privateApis});var ie=O((Gr,gt)=>{gt.exports=window.wp.data});var se=O((zr,mt)=>{mt.exports=window.wp.coreData});var ht=O((Mr,vt)=>{vt.exports=window.wp.url});function Xe(e){var t,n,o="";if(typeof e=="string"||typeof e=="number")o+=e;else if(typeof e=="object")if(Array.isArray(e)){var r=e.length;for(t=0;t(0,Ze.jsx)(o,{ref:a,className:C("admin-ui-navigable-region",t),"aria-label":n,role:"region",tabIndex:"-1",...r,children:e}));Ce.displayName="NavigableRegion";var Ee=Ce;var Ke=s(E(),1),We={};function ge(e,t){let n=Ke.useRef(We);return n.current===We&&(n.current=e(t)),n}function me(e,...t){let n=new URL(`https://base-ui.com/production-error/${e}`);return t.forEach(o=>n.searchParams.append("args[]",o)),`Base UI error #${e}; visit ${n} for the full message.`}var oe=s(E(),1);function ve(e,t,n,o){let r=ge(ke).current;return Ct(r,e,t,n,o)&&Ue(r,[e,t,n,o]),r.callback}function Ie(e){let t=ge(ke).current;return Et(t,e)&&Ue(t,e),t.callback}function ke(){return{callback:null,cleanup:null,refs:[]}}function Ct(e,t,n,o,r){return e.refs[0]!==t||e.refs[1]!==n||e.refs[2]!==o||e.refs[3]!==r}function Et(e,t){return e.refs.length!==t.length||e.refs.some((n,o)=>n!==t[o])}function Ue(e,t){if(e.refs=t,t.every(n=>n==null)){e.callback=null;return}e.callback=n=>{if(e.cleanup&&(e.cleanup(),e.cleanup=null),n!=null){let o=Array(t.length).fill(null);for(let r=0;r{for(let r=0;r=e}function he(e){if(!Fe.isValidElement(e))return null;let t=e,n=t.props;return(Je(19)?n?.ref:t.ref)??null}function U(e,t){if(e&&!t)return e;if(!e&&t)return t;if(e||t)return{...e,...t}}function _e(e,t){let n={};for(let o in e){let r=e[o];if(t?.hasOwnProperty(o)){let a=t[o](r);a!=null&&Object.assign(n,a);continue}r===!0?n[`data-${o.toLowerCase()}`]="":r&&(n[`data-${o.toLowerCase()}`]=r.toString())}return n}function $e(e,t){return typeof e=="function"?e(t):e}function et(e,t){return typeof e=="function"?e(t):e}var J={};function A(e,t,n,o,r){let a={...Pe(e,J)};return t&&(a=Q(a,t)),n&&(a=Q(a,n)),o&&(a=Q(a,o)),r&&(a=Q(a,r)),a}function tt(e){if(e.length===0)return J;if(e.length===1)return Pe(e[0],J);let t={...Pe(e[0],J)};for(let n=1;n=65&&r<=90&&(typeof t=="function"||typeof t>"u")}function nt(e){return typeof e=="function"}function Pe(e,t){return nt(e)?e(t):e??J}function It(e,t){return t?e?n=>{if(Ut(n)){let r=n;kt(r);let a=t(r);return r.baseUIHandlerPrevented||e?.(r),a}let o=t(n);return e?.(n),o}:t:e}function kt(e){return e.preventBaseUIHandler=()=>{e.baseUIHandlerPrevented=!0},e}function be(e,t){return t?e?t+" "+e:t:e}function Ut(e){return e!=null&&typeof e=="object"&&"nativeEvent"in e}var Qt=Object.freeze([]),R=Object.freeze({});var we=s(E(),1);function rt(e,t,n={}){let o=t.render,r=Jt(t,n);if(n.enabled===!1)return null;let a=n.state??R;return Ft(e,o,r,a)}function Jt(e,t={}){let{className:n,style:o,render:r}=e,{state:a=R,ref:i,props:c,stateAttributesMapping:u,enabled:d=!0}=t,f=d?$e(n,a):void 0,g=d?et(o,a):void 0,D=d?_e(a,u):R,p=d?U(D,Array.isArray(c)?tt(c):c)??R:R;return typeof document<"u"&&(d?Array.isArray(i)?p.ref=Ie([p.ref,he(r),...i]):p.ref=ve(p.ref,he(r),i):ve(null,null)),d?(f!==void 0&&(p.className=be(p.className,f)),g!==void 0&&(p.style=U(p.style,g)),p):R}function Ft(e,t,n,o){if(t){if(typeof t=="function")return t(n,o);let r=A(n,t.props);return r.ref=n.ref,oe.cloneElement(t,r)}if(e&&typeof e=="string")return _t(e,n);throw new Error(me(8))}function _t(e,t){return e==="button"?(0,we.createElement)("button",{type:"button",...t,key:t.key}):e==="img"?(0,we.createElement)("img",{alt:"",...t,key:t.key}):oe.createElement(e,t)}function ae(e){return rt(e.defaultTagName??"div",e,e)}var at=s(H(),1);if(typeof document<"u"&&!document.head.querySelector("style[data-wp-hash='244b5c59c0']")){let e=document.createElement("style");e.setAttribute("data-wp-hash","244b5c59c0"),e.appendChild(document.createTextNode('@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;@layer wp-ui-components{._96e6251aad1a6136__badge{border-radius:var(--wpds-border-radius-lg,8px);font-family:var(--wpds-font-family-body,-apple-system,system-ui,"Segoe UI","Roboto","Oxygen-Sans","Ubuntu","Cantarell","Helvetica Neue",sans-serif);font-size:var(--wpds-font-size-sm,12px);font-weight:var(--wpds-font-weight-regular,400);line-height:var(--wpds-font-line-height-xs,16px);padding-block:var(--wpds-dimension-padding-xs,4px);padding-inline:var(--wpds-dimension-padding-sm,8px)}._99f7158cb520f750__is-high-intent{background-color:var(--wpds-color-bg-surface-error,#f6e6e3);color:var(--wpds-color-fg-content-error,#470000)}.c20ebef2365bc8b7__is-medium-intent{background-color:var(--wpds-color-bg-surface-warning,#fde6bd);color:var(--wpds-color-fg-content-warning,#2e1900)}._365e1626c6202e52__is-low-intent{background-color:var(--wpds-color-bg-surface-caution,#fee994);color:var(--wpds-color-fg-content-caution,#281d00)}._33f8198127ddf4ef__is-stable-intent{background-color:var(--wpds-color-bg-surface-success,#c5f7cc);color:var(--wpds-color-fg-content-success,#002900)}._04c1aca8fc449412__is-informational-intent{background-color:var(--wpds-color-bg-surface-info,#deebfa);color:var(--wpds-color-fg-content-info,#001b4f)}._90726e69d495ec19__is-draft-intent{background-color:var(--wpds-color-bg-surface-neutral-weak,#f0f0f0);color:var(--wpds-color-fg-content-neutral,#1e1e1e)}._898f4a544993bd39__is-none-intent{background-color:var(--wpds-color-bg-surface-neutral,#f8f8f8);color:var(--wpds-color-fg-content-neutral-weak,#6d6d6d)}}')),document.head.appendChild(e)}var ot={badge:"_96e6251aad1a6136__badge","is-high-intent":"_99f7158cb520f750__is-high-intent","is-medium-intent":"c20ebef2365bc8b7__is-medium-intent","is-low-intent":"_365e1626c6202e52__is-low-intent","is-stable-intent":"_33f8198127ddf4ef__is-stable-intent","is-informational-intent":"_04c1aca8fc449412__is-informational-intent","is-draft-intent":"_90726e69d495ec19__is-draft-intent","is-none-intent":"_898f4a544993bd39__is-none-intent"},Le=(0,at.forwardRef)(function({children:t,intent:n="none",render:o,className:r,...a},i){return ae({render:o,defaultTagName:"span",ref:i,props:A(a,{className:C(ot.badge,ot[`is-${n}-intent`],r),children:t})})});var lt=s(H(),1);if(typeof document<"u"&&!document.head.querySelector("style[data-wp-hash='71d20935c2']")){let e=document.createElement("style");e.setAttribute("data-wp-hash","71d20935c2"),e.appendChild(document.createTextNode("@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;@layer wp-ui-components{._19ce0419607e1896__stack{display:flex}}")),document.head.appendChild(e)}var $t={stack:"_19ce0419607e1896__stack"},en={xs:"var(--wpds-dimension-gap-xs, 4px)",sm:"var(--wpds-dimension-gap-sm, 8px)",md:"var(--wpds-dimension-gap-md, 12px)",lg:"var(--wpds-dimension-gap-lg, 16px)",xl:"var(--wpds-dimension-gap-xl, 24px)","2xl":"var(--wpds-dimension-gap-2xl, 32px)","3xl":"var(--wpds-dimension-gap-3xl, 40px)"},W=(0,lt.forwardRef)(function({direction:t,gap:n,align:o,justify:r,wrap:a,render:i,...c},u){let d={gap:n&&en[n],alignItems:o,justifyContent:r,flexDirection:t,flexWrap:a};return ae({render:i,ref:u,props:A(c,{style:d,className:$t.stack})})});var ct=s(k(),1),{Fill:dt,Slot:ut}=(0,ct.createSlotFill)("SidebarToggle");var w=s(re(),1);function ft({headingLevel:e=2,breadcrumbs:t,badges:n,title:o,subTitle:r,actions:a,showSidebarToggle:i=!0}){let c=`h${e}`;return(0,w.jsxs)(W,{direction:"column",className:"admin-ui-page__header",render:(0,w.jsx)("header",{}),children:[(0,w.jsxs)(W,{direction:"row",justify:"space-between",gap:"sm",children:[(0,w.jsxs)(W,{direction:"row",gap:"sm",align:"center",justify:"start",children:[i&&(0,w.jsx)(ut,{bubblesVirtually:!0,className:"admin-ui-page__sidebar-toggle-slot"}),o&&(0,w.jsx)(c,{className:"admin-ui-page__header-title",children:o}),t,n]}),(0,w.jsx)(W,{direction:"row",gap:"sm",style:{width:"auto",flexShrink:0},className:"admin-ui-page__header-actions",align:"center",children:a})]}),r&&(0,w.jsx)("p",{className:"admin-ui-page__header-subtitle",children:r})]})}var F=s(re(),1);function pt({headingLevel:e,breadcrumbs:t,badges:n,title:o,subTitle:r,children:a,className:i,actions:c,hasPadding:u=!1,showSidebarToggle:d=!0}){let f=C("admin-ui-page",i);return(0,F.jsxs)(Ee,{className:f,ariaLabel:o,children:[(o||t||n)&&(0,F.jsx)(ft,{headingLevel:e,breadcrumbs:t,badges:n,title:o,subTitle:r,actions:c,showSidebarToggle:d}),u?(0,F.jsx)("div",{className:"admin-ui-page__content has-padding",children:a}):a]})}pt.SidebarToggleFill=dt;var ye=pt;var y=s(k()),Ht=s(ie()),Rt=s(H()),Z=s(I()),qt=s(se());import{privateApis as fn}from"@wordpress/connectors";if(typeof document<"u"&&!document.head.querySelector("style[data-wp-hash='1b00f16b8d']")){let e=document.createElement("style");e.setAttribute("data-wp-hash","1b00f16b8d"),e.appendChild(document.createTextNode(".connectors-page{box-sizing:border-box;margin:0 auto;max-width:680px;padding:24px;width:100%}.connectors-page .components-item{background:#fff;border:1px solid #ddd;border-radius:8px;overflow:hidden;padding:20px;scroll-margin-top:120px}.connectors-page .connector-settings__error{color:#cc1818}.connectors-page .connector-settings .components-text-control__input{font-family:monospace;scroll-margin-top:120px}.connectors-page--empty{align-items:center;display:flex;flex-direction:column;flex-grow:1;gap:32px;justify-content:center;text-align:center}.connectors-page .ai-plugin-callout{background:linear-gradient(90deg,#fff9,#fff9),linear-gradient(90deg,#89dcdc,#c7eb5c 46.15%,#a920c1);border-radius:8px;overflow:hidden;padding:24px;padding-inline-end:220px;position:relative}[dir=rtl] .connectors-page .ai-plugin-callout{background:linear-gradient(270deg,#fff9,#fff9),linear-gradient(270deg,#89dcdc,#c7eb5c 46.15%,#a920c1)}.connectors-page .ai-plugin-callout__content{align-items:flex-start;display:flex;flex-direction:column;gap:12px;padding-top:2px}.connectors-page .ai-plugin-callout__content p{font-size:13px;line-height:20px;margin:0}.connectors-page .ai-plugin-callout__decoration{height:248px;inset-inline-end:8px;position:absolute;top:-15px;width:248px}.connectors-page>p{color:#949494;text-align:center}@media (max-width:680px){.connectors-page .ai-plugin-callout{padding:12px;padding-inline-end:84px}.connectors-page .ai-plugin-callout__decoration{height:134px;inset-inline-end:4px;top:-8px;width:134px}}@media (max-width:480px){.connectors-page{padding:8px}.connectors-page .components-item{padding:12px}.connectors-page .components-item>.components-v-stack>.components-h-stack:first-child svg{height:32px;width:32px}.connectors-page .components-item>.components-v-stack>.components-h-stack:first-child>.components-h-stack:last-child{align-items:flex-end;flex-direction:column}}")),document.head.appendChild(e)}var ee=s(k()),Oe=s(se()),ue=s(ie()),L=s(H()),m=s(I()),Ot=s(ht());import{speak as de}from"@wordpress/a11y";var ce=s(k()),$=s(H()),Ge=s(I());import{__experimentalRegisterConnector as tn,__experimentalConnectorItem as nn,__experimentalDefaultConnectorSettings as rn}from"@wordpress/connectors";var xe=s(se()),le=s(ie()),_=s(H()),l=s(I());import{speak as S}from"@wordpress/a11y";function Pt({file:e,settingName:t,connectorName:n,isInstalled:o,isActivated:r,keySource:a="none",initialIsConnected:i=!1}){let[c,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(!1),[g,D]=(0,_.useState)(i),[p,B]=(0,_.useState)(null),h=e?.replace(/\.php$/,""),x=h?.includes("/")?h.split("/")[0]:h,{derivedPluginStatus:P,canManagePlugins:G,currentApiKey:v,canInstallPlugins:z}=(0,le.useSelect)(N=>{let X=N(xe.store),K=X.getEntityRecord("root","site")?.[t]??"",Y=!!X.canUser("create",{kind:"root",name:"plugin"});if(!e)return{derivedPluginStatus:X.hasFinishedResolution("getEntityRecord",["root","site"])?"active":"checking",canManagePlugins:void 0,currentApiKey:K,canInstallPlugins:Y};let Re=X.getEntityRecord("root","plugin",h);if(!X.hasFinishedResolution("getEntityRecord",["root","plugin",h]))return{derivedPluginStatus:"checking",canManagePlugins:void 0,currentApiKey:K,canInstallPlugins:Y};if(Re)return{derivedPluginStatus:Re.status==="active"?"active":"inactive",canManagePlugins:!0,currentApiKey:K,canInstallPlugins:Y};let pe="not-installed";return r?pe="active":o&&(pe="inactive"),{derivedPluginStatus:pe,canManagePlugins:!1,currentApiKey:K,canInstallPlugins:Y}},[h,t,o,r]),b=p??P,j=G,q=b==="active"&&g||p==="active"&&!!v,{saveEntityRecord:M,invalidateResolution:T}=(0,le.useDispatch)(xe.store),fe=async()=>{if(x){f(!0);try{await M("root","plugin",{slug:x,status:"active"},{throwOnError:!0}),B("active"),T("getEntityRecord",["root","site"]),u(!0),S((0,l.sprintf)((0,l.__)("Plugin for %s installed and activated successfully."),n))}catch{S((0,l.sprintf)((0,l.__)("Failed to install plugin for %s."),n),"assertive")}finally{f(!1)}}},te=async()=>{if(e){f(!0);try{await M("root","plugin",{plugin:h,status:"active"},{throwOnError:!0}),B("active"),T("getEntityRecord",["root","site"]),u(!0),S((0,l.sprintf)((0,l.__)("Plugin for %s activated successfully."),n))}catch{S((0,l.sprintf)((0,l.__)("Failed to activate plugin for %s."),n),"assertive")}finally{f(!1)}}};return{pluginStatus:b,canInstallPlugins:z,canActivatePlugins:j,isExpanded:c,setIsExpanded:u,isBusy:d,isConnected:q,currentApiKey:v,keySource:a,handleButtonClick:()=>{if(b==="not-installed"){if(z===!1)return;fe()}else if(b==="inactive"){if(j===!1)return;te()}else u(!c)},getButtonLabel:()=>{if(d)return b==="not-installed"?(0,l.__)("Installing\u2026"):(0,l.__)("Activating\u2026");if(c)return(0,l.__)("Cancel");if(q)return(0,l.__)("Edit");switch(b){case"checking":return(0,l.__)("Checking\u2026");case"not-installed":return(0,l.__)("Install");case"inactive":return(0,l.__)("Activate");case"active":return(0,l.__)("Set up")}},saveApiKey:async N=>{let X=v;try{let Y=(await M("root","site",{[t]:N},{throwOnError:!0}))?.[t];if(N&&(Y===X||!Y))throw new Error("It was not possible to connect to the provider using this key.");D(!0),S((0,l.sprintf)((0,l.__)("%s connected successfully."),n))}catch(ne){throw console.error("Failed to save API key:",ne),ne}},removeApiKey:async()=>{try{await M("root","site",{[t]:""},{throwOnError:!0}),D(!1),S((0,l.sprintf)((0,l.__)("%s disconnected."),n))}catch(N){throw console.error("Failed to remove API key:",N),S((0,l.sprintf)((0,l.__)("Failed to disconnect %s."),n),"assertive"),N}}}}var bt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364l2.0201-1.1685a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.4043-.6813zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z",fill:"currentColor"})),wt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 32 32",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M6.2 21.024L12.416 17.536L12.52 17.232L12.416 17.064H12.112L11.072 17L7.52 16.904L4.44 16.776L1.456 16.616L0.704 16.456L0 15.528L0.072 15.064L0.704 14.64L1.608 14.72L3.608 14.856L6.608 15.064L8.784 15.192L12.008 15.528H12.52L12.592 15.32L12.416 15.192L12.28 15.064L9.176 12.96L5.816 10.736L4.056 9.456L3.104 8.808L2.624 8.2L2.416 6.872L3.28 5.92L4.44 6L4.736 6.08L5.912 6.984L8.424 8.928L11.704 11.344L12.184 11.744L12.376 11.608L12.4 11.512L12.184 11.152L10.4 7.928L8.496 4.648L7.648 3.288L7.424 2.472C7.344 2.136 7.288 1.856 7.288 1.512L8.272 0.176L8.816 0L10.128 0.176L10.68 0.656L11.496 2.52L12.816 5.456L14.864 9.448L15.464 10.632L15.784 11.728L15.904 12.064H16.112V11.872L16.28 9.624L16.592 6.864L16.896 3.312L17 2.312L17.496 1.112L18.48 0.464L19.248 0.832L19.88 1.736L19.792 2.32L19.416 4.76L18.68 8.584L18.2 11.144H18.48L18.8 10.824L20.096 9.104L22.272 6.384L23.232 5.304L24.352 4.112L25.072 3.544H26.432L27.432 5.032L26.984 6.568L25.584 8.344L24.424 9.848L22.76 12.088L21.72 13.88L21.816 14.024L22.064 14L25.824 13.2L27.856 12.832L30.28 12.416L31.376 12.928L31.496 13.448L31.064 14.512L28.472 15.152L25.432 15.76L20.904 16.832L20.848 16.872L20.912 16.952L22.952 17.144L23.824 17.192H25.96L29.936 17.488L30.976 18.176L31.6 19.016L31.496 19.656L29.896 20.472L27.736 19.96L22.696 18.76L20.968 18.328H20.728V18.472L22.168 19.88L24.808 22.264L28.112 25.336L28.28 26.096L27.856 26.696L27.408 26.632L24.504 24.448L23.384 23.464L20.848 21.328H20.68V21.552L21.264 22.408L24.352 27.048L24.512 28.472L24.288 28.936L23.488 29.216L22.608 29.056L20.8 26.52L18.936 23.664L17.432 21.104L17.248 21.208L16.36 30.768L15.944 31.256L14.984 31.624L14.184 31.016L13.76 30.032L14.184 28.088L14.696 25.552L15.112 23.536L15.488 21.032L15.712 20.2L15.696 20.144L15.512 20.168L13.624 22.76L10.752 26.64L8.48 29.072L7.936 29.288L6.992 28.8L7.08 27.928L7.608 27.152L10.752 23.152L12.648 20.672L13.872 19.24L13.864 19.032H13.792L5.44 24.456L3.952 24.648L3.312 24.048L3.392 23.064L3.696 22.744L6.208 21.016L6.2 21.024Z",fill:"#D97757"})),Lt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 32 32",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M0 4C0 1.79086 1.79086 0 4 0H28C30.2091 0 32 1.79086 32 4V28C32 30.2091 30.2091 32 28 32H4C1.79086 32 0 30.2091 0 28V4Z",fill:"#F0F0F0"}),React.createElement("path",{d:"M14.5 8V12H17.5V8H19V12H20.5C20.7652 12 21.0196 12.1054 21.2071 12.2929C21.3946 12.4804 21.5 12.7348 21.5 13V17L18.5 21V23C18.5 23.2652 18.3946 23.5196 18.2071 23.7071C18.0196 23.8946 17.7652 24 17.5 24H14.5C14.2348 24 13.9804 23.8946 13.7929 23.7071C13.6054 23.5196 13.5 23.2652 13.5 23V21L10.5 17V13C10.5 12.7348 10.6054 12.4804 10.7929 12.2929C10.9804 12.1054 11.2348 12 11.5 12H13V8H14.5ZM15 20.5V22.5H17V20.5L20 16.5V13.5H12V16.5L15 20.5Z",fill:"#949494"})),yt=()=>React.createElement("svg",{width:"40",height:"40",viewBox:"0 0 44 44",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("rect",{width:"44",height:"44",fill:"#357B49",rx:"6"}),React.createElement("path",{fill:"#fff",fillRule:"evenodd",d:"m29.746 28.31-6.392-16.797c-.152-.397-.305-.672-.789-.675-.673 0-1.408.611-1.746 1.316l-7.378 16.154c-.072.16-.143.311-.214.454-.5.995-1.045 1.546-2.357 1.626a.399.399 0 0 0-.16.033l-.01.004a.399.399 0 0 0-.23.392v.01c0 .054.01.106.03.155l.004.01a.416.416 0 0 0 .394.252h6.212a.417.417 0 0 0 .307-.12.416.416 0 0 0 .124-.305.398.398 0 0 0-.105-.302.399.399 0 0 0-.294-.127c-.757 0-2.197-.062-2.197-1.164.02-.318.103-.63.245-.916l1.399-3.152c.52-1.163 1.654-1.163 2.572-1.163h5.843c.023 0 .044 0 .062.003.13.014.16.081.214.242l1.534 4.07a2.857 2.857 0 0 1 .216 1.04c0 .054-.003.104-.01.153-.09.726-.831.887-1.49.887a.4.4 0 0 0-.294.127l-.007.008-.007.008a.401.401 0 0 0-.092.286v.01c0 .054.01.106.03.155l.005.01a.42.42 0 0 0 .395.252h7.011a.413.413 0 0 0 .279-.13.412.412 0 0 0 .11-.297.387.387 0 0 0-.09-.294.388.388 0 0 0-.277-.135c-1.448-.122-2.295-.643-2.847-2.08Zm-11.985-5.844 2.847-6.304c.361-.728.659-1.486.889-2.265 0-.06.03-.092.06-.092s.061.032.061.091c.02.122.045.247.073.374.197.888.584 1.878.914 2.723l.176.453 1.684 4.529a.927.927 0 0 1 .092.4.473.473 0 0 1-.009.094c-.041.202-.228.272-.602.272h-6.063c-.122 0-.184-.03-.184-.092a.36.36 0 0 1 .062-.183Zm17.107-.721c0 .786-.446 1.231-1.25 1.231-.806 0-1.125-.409-1.125-1.034 0-.786.465-1.231 1.25-1.231.785 0 1.125.427 1.125 1.034ZM9.629 23.002c.803 0 1.25-.447 1.25-1.231 0-.607-.343-1.036-1.128-1.036-.785 0-1.25.447-1.25 1.231 0 .625.325 1.036 1.128 1.036Z",clipRule:"evenodd"})),xt=()=>React.createElement("svg",{width:"40",height:"40",style:{flex:"none",lineHeight:1},viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true"},React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"#3186FF"}),React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"url(#lobe-icons-gemini-fill-0)"}),React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"url(#lobe-icons-gemini-fill-1)"}),React.createElement("path",{d:"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z",fill:"url(#lobe-icons-gemini-fill-2)"}),React.createElement("defs",null,React.createElement("linearGradient",{gradientUnits:"userSpaceOnUse",id:"lobe-icons-gemini-fill-0",x1:"7",x2:"11",y1:"15.5",y2:"12"},React.createElement("stop",{stopColor:"#08B962"}),React.createElement("stop",{offset:"1",stopColor:"#08B962",stopOpacity:"0"})),React.createElement("linearGradient",{gradientUnits:"userSpaceOnUse",id:"lobe-icons-gemini-fill-1",x1:"8",x2:"11.5",y1:"5.5",y2:"11"},React.createElement("stop",{stopColor:"#F94543"}),React.createElement("stop",{offset:"1",stopColor:"#F94543",stopOpacity:"0"})),React.createElement("linearGradient",{gradientUnits:"userSpaceOnUse",id:"lobe-icons-gemini-fill-2",x1:"3.5",x2:"17.5",y1:"13.5",y2:"12"},React.createElement("stop",{stopColor:"#FABC12"}),React.createElement("stop",{offset:".46",stopColor:"#FABC12",stopOpacity:"0"}))));function ze(){try{return JSON.parse(document.getElementById("wp-script-module-data-options-connectors-wp-admin")?.textContent??"")?.connectors??{}}catch{return{}}}var on={google:xt,openai:bt,anthropic:wt,akismet:yt};function an(e,t){if(t)return React.createElement("img",{src:t,alt:"",width:40,height:40});let n=on[e];return React.createElement(n||Lt,null)}var sn=()=>React.createElement("span",{style:{color:"#345b37",backgroundColor:"#eff8f0",padding:"4px 12px",borderRadius:"2px",fontSize:"13px",fontWeight:500,whiteSpace:"nowrap"}},(0,Ge.__)("Connected")),ln=()=>React.createElement(Le,null,(0,Ge.__)("Not available"));function cn({name:e,description:t,logo:n,authentication:o,plugin:r}){let a=o?.method==="api_key"?o:void 0,i=a?.settingName??"",c=a?.credentialsUrl??void 0,u=r?.file?.replace(/\.php$/,""),d=u?.includes("/")?u.split("/")[0]:u,f;try{c&&(f=new URL(c).hostname)}catch{}let{pluginStatus:g,canInstallPlugins:D,canActivatePlugins:p,isExpanded:B,setIsExpanded:h,isBusy:x,isConnected:P,currentApiKey:G,keySource:v,handleButtonClick:z,getButtonLabel:b,saveApiKey:j,removeApiKey:q}=Pt({file:r?.file,settingName:i,connectorName:e,isInstalled:r?.isInstalled,isActivated:r?.isActivated,keySource:a?.keySource,initialIsConnected:a?.isConnected}),M=v==="env"||v==="constant",T=g==="not-installed"&&D===!1||g==="inactive"&&p===!1,fe=!T,te=(0,$.useRef)(null),V=(0,$.useRef)(!1);(0,$.useEffect)(()=>{V.current&&!x&&(V.current=!1,te.current?.focus())},[x,B,P]);let je=()=>{(g==="not-installed"||g==="inactive")&&(V.current=!0),z()};return React.createElement(nn,{className:d?`connector-item--${d}`:void 0,logo:n,name:e,description:t,actionArea:React.createElement(ce.__experimentalHStack,{spacing:3,expanded:!1},P&&React.createElement(sn,null),T&&React.createElement(ln,null),fe&&React.createElement(ce.Button,{ref:te,variant:B||P?"tertiary":"secondary",size:"compact",onClick:je,disabled:g==="checking"||x,isBusy:x},b()))},B&&g==="active"&&React.createElement(rn,{key:P?"connected":"setup",initialValue:M?"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022":G,helpUrl:c,helpLabel:f,readOnly:P||M,keySource:v,onRemove:M?void 0:async()=>{V.current=!0;try{await q()}catch{V.current=!1}},onSave:async He=>{await j(He),V.current=!0,h(!1)}}))}function Gt(){let e=ze(),t=n=>n.replace(/[^a-z0-9-_]/gi,"-");for(let[n,o]of Object.entries(e)){if(n==="akismet"&&!o.plugin?.isInstalled)continue;let{authentication:r}=o,a=t(n),i={name:o.name,description:o.description,type:o.type,logo:an(n,o.logoUrl),authentication:r,plugin:o.plugin};r.method==="api_key"&&(i.render=cn),tn(a,i)}}function zt(){return React.createElement("div",{className:"ai-plugin-callout__decoration","aria-hidden":"true"},React.createElement("svg",{viewBox:"0 0 248 248",xmlns:"http://www.w3.org/2000/svg",xmlnsXlink:"http://www.w3.org/1999/xlink",focusable:"false",style:{width:"100%",height:"100%"}},React.createElement("image",{href:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPgAAAD4CAYAAADB0SsLAACAAElEQVR4XuzdB7hlRZEH8D73zRBniJLDzBAEVFQMKCaCWXENa1oTYM45hwXEtOa0ZgVzWnPOBHPWVcxgzjnrGvb/O91n5s5lZnjAe4Bw6vvqO3XPPed0rO6q6urqUkYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUYYYYQRRhhhhBFGGGGEEUaYD3RdtxY9/XuEEUb4F4aBoWdxhBFG+BeHxsxg0+BmwSXBrYPbBTfOI3Dz4NKZV0cYYYTFhMlkskGcm5tbC6f/m5mtlwavGLxacKt8+k7BY4N7B/cLHhbceTrtaRi+A2bTX7JkSY9Lly4tG220UY8jjDDCmcAsM8/iLHOfCYObtS8bPCi4ZfCIJPHw4J6lMvlVgzuslYEpmP7WbPoDc08z+MYbEwpGGGGEdcIUo24V3CG4WXB5o103R4fBtgpuFkRvm3ubBrcPXiTMuElw+66K4uhdgrt3VUx33Se4PMltF1wRREOMTmTfrNHL2/s7BLfMtzdq+dgmaW4UBt8+zH2RYHh7o+2C24fBl45MPsKFGsx8U4w8O+MO918U/FnwLsGH5f5Pcz0ueJT7YbAXB68f/HnwncGr5pmfBD/Z1Rn79OA3ggcEP5N3fpzrFYLvDqKvkaycEPxx8BbBxwd/Erxv8F6NflKeu0lL+1W5HtzS/lDKcLlcfxLm/lLw0mHq0zbZZJOf5rqPmVwZlWWEES5UQJwddNdp0RozTGPuvTb4z+B9MXbu/SPXpwbv6n7efV3wxo3+SPDQPPPP4Le6ysi/C/46ePk8813Phb5K8FPtuesnO28K/jNIbH9Wox8RfEijn5fnbtWef2fwmi1Pn096Vwz+I+X4Ucp0uTD1bzbddNN/ht5vGMC6kcFHuDCBma3prBuHCTYJg4QPJht1VQyeBKfp/YJmZeL1KnSe3TO4W967Wt6/WK475nrl4KVCbx28XJ7bv6u6tpkbbtHumdXd913PbVOqHn75UkX13RvN4LYjOs+sCBL56e/7JO1tglcJXiq4ZdK7StK+fHDLlOmg4JVCL8/9jfO/ciwp1VK/SamWela4gR5hhH9tMEPDaWNUcJsw+qeDX8l/FwsjvC2McGoQc704+OXQ1w4eG/x88NbBu6Pz7IOCN897X8w3n5ZvHZDvvCLX/8zvTlrDzDkfPDNoz+2bb94r12sEr9TyQbowaHw2ab4vaNY+JfhJ94Nme2U6KJ95WfDLwesGn9ToO86kMe88jTDC+QKGDovh4MDowR2iqxJ/idaXy3+no/Psobme0uj/yPWVjX5g8AnoPPtszIbOd94ZvFrE4q9E7315mHzO9+fL5GcGU8/K49OCBprrlZqnrwWv1OhfJs0Dg3/xO1ez/XfRQXr+xxp9y+DrG82KP8II/5owy0wzDM76fI3gdXOPZZoYfr2uOqBcIa/TjYnIl87vfwvuEbxYoy+e5/fMezcMQxONt8v1KpnFLxWcLOQ69FT+ifX7BnfuqjX9Bl1VF1juD09erhFkVb9O8Lru5//Duqrjs8QbCAwMuwUvU2r5LjqV1AgjnH9hihFW/27XywQ/GDw+nX6fMOP7g28JE+yR6+uC7wi9V/573qSKtEReDLRvXqc3n2GggNODxSD2L5aTyWzasygv68Lh/wZPDb6j1PX2Bzf634J3Dn6wXW/caA44PQzvD+mMMML5Da5Vqgj7veAV0WFCIuwBYc5eRE/HZeH+YXuOjnux0Ga/nWaZaejomHuawReLuQeYzcNsfoY8zTJ4A5z5pVLF8lsF39zoBwSf0eint9/o99TXRhjhfA6YNJfb5nqjdPrtgrcOI9wiuG3wdsE7BTmi3Cx4Z88Ht+qqKMx3fPaT/4qAwc3OdwnuFbx68K7BSwYvGzyyVMs9l1m0/0cY4XwFNwy+vNROzGqMfmhwn+DzgkeHWVeEwZ87qcaqLaZnu2HGm8WzA9Oz5/R31kcPvwfw/rruLxIwuj0/eJMgAyP69qUyvDq0Hq8O0U8pdbltLVhMyWWEEQZ4bKni5f+U6jCC/mTw2o3+QddEdBgm2nl9DA7OBcY6v8BLSq2TZwYf1GgiPFEe/flSZ3T0r4LLvDSoJwNSU0YYYTHhysFHBm8QvHipS0A6KSeSxwTvE6bdNddjgo/oquPJWoys016QYT2D1+Glusdep1TJB81llpFRHd62VOebhwXvHrRpZrXOf27ZIEa48IClK3qkjRjEcmvTDGkHBh9VqmWYkQx9ZH1l3TDfWXp9IvN63t++VAeSm5Wa12NKzQtbgBmSpIFhMMvjSl2qunWjDyiV4dCHBA8uleGIzxcpVRrxzFowSCDrgvXdnwdcotR8367U/KI5+rBNPC7fPSYMbpPL7cLYd2uOQz3DjzDCOQEd3Hout84TShUd6YZ0xVkR/Yv1lTUwzNrrYc6FgP2Dny1VzLXe3KsEwcsFf9doS1XfCP6j1AHJ0pT79ogTmf8ePK7UwcH915VqACNGW85aLyxg2ejhsyL6T7vq6deXKcy8f5j6pE033fSrYfCLmskHS/4II5xdGLZW8qUmSjIKEcuJmK8olUno2oOBqIeh029otlsgMIPfo1T1gD/5k4P/Fdyl1PVlTLqy1CWp/y6Vce8QfG6pUsi/l2oUNFsLAoE2w29bahkNGgsK6xkQBkMlMZ2R7YTgf3V1m+szU4dPCYPvFLxjGPu+uVqVWMueMcII8wE9Rae3rLOqVLdLsxjGMCuarV0XC8xe0iOyWlIygPB00+mJ4oeWyryYlHcYRqS33jCdfMtcbx68aegt3CuV8bcO8nO/TakDAi+62wQNXNJwn5pBD3bfrOm/I0stP3FfntSLAU+euLDauOKZO4R23wBoVcHz5xiadGD14XbB/5irnnP/Eea+Q64XsfyY+3fLM7u1Z2c/McIIZxAzWb6+XqpYiFne1ej7lDq7oc1+iwU8vqTB+8tMTHx+TqmztPtvK1XMJm5/IXi1dv/npTJrL86mPGbe37bf3Ea/1u7Tud/X7hOLbXRBm+3/s9FmUrow+pRSZ3fpnV6qtOL+H/MtA92QnkHoR+05g9Q5htYuBp4+jTD0AWHovzVaOKre9z3X6w9tODL5CGvBdMeY6hyDm6XObI80mrhqVqe/mqV6GN5xXSBRnGHs7aXq/WbEt5bKbGZutIGG3v2WUq31DID2dxt0VgRfXarqwAf8Bf5L3jxjR5fBgbOJMtnNZnAgCbjPsGbGRyufVQK0lQLSBPppeWfP4JuDx3d1nf9Vwf9xv9SBSB4x5VmGaVF7qk12Db43/705TL1H8JXBtwf3zr1nd3W/uu2x62rHES7MMHSorsYvu2ZXN3YQec1GGMymiUuXKgqvnHp1IQDD0uUFRJSeWVl6VAGDCesxhmWxX1mqVZuOjJHAwaUyHkeQQ4IHdjViKoOaAA8bdzXo4qHBTXOPjzy3WKK7TSzKy6Nuj/y+VlfFXGkS5cVss3nEzM1X3juet1dc+KerT+r+cOGiRHs5LLh5V7eV+haVgE6tTMR1hsobBQ0O82bA4VntBMPUS+fqJhcbXOxLt/nlepO60UVZb9xVnX3eaYxwAQVLLHPNvzsdxM6pX5Qq7mE8Vmli4U1LnbnQROaFAmrAD0r9rln0w6WKtjdP+sN20fsHe2eaXPlv03s9/4ZSBwD3P93Vvdfu/zC0wQj9f6EvGSS29+JzcPANx8CD2kEyMPN7hnhu7RnNsv4f7ZkPlDVWbdtFe7E8dfbb1N2l+dn7PVe3wH6/vW8g+kR7h5pjpQH98K4xrHqfdlxZH3h+aKfgdhtvvHGfXt65Qn6fjs73rpPnTkbnehfvDOmMcCEEwQEHRwmdKx3FTETU+2yudNeXlbrkpaNaEvvfUg1ZPeg8CwBvDH66VJH52UEhlQ4OPjJ5+EyuDGW37yoT37OreqZnHt3VKC1ooZVEakG/MbgyeFLwg/nGykkNKuG/iwZPaGl4l3X60/nfrHz/dp//PMOZ9B7c1YHuU8Gnhr6U+6FfkXeIxp9Knb07uGrp0qUnpQ4/HnqvoDzIuy2vVht865Cg0FPu3xbTDQw+MPlArw+mvNe2Tpt9IvjV/L543nlTvvXloNmbuP7ZIGPj6ll/ZPILGWDuhkvSUQ5Ip7l0OstmwUunMxwYXJYOQty0AYQ/pB6i982bq3WwBt4jRhO7fcsVQ6PttSY2C9dEdL3spEZS3SV4mUn1Xb9IkGHJdVlQHncKinluhqYDA9FULhrsgqLFQExz0eAlQ4sRtYf352pYpV2DB4QWqdWGGOltHbRP/TK5L4KqjS/S3j3oHXqu0FG+JVSUaC6CTeyfOrxUrsRn4aSUw7sGgsvP1b3vK3O9Yq7b57rLXA0UsWve2d5MHNx72AK7Phj+D05auiLZTFp+klx38eCVJrWuVoW+wmTKPXiECwFMMTfcWbDAXP8RRr9sOk5vkU2nob/qLFfv6tLPmcLQiQbs1uiAK0oVTX2bB9mPct9vrq1f9p+OGHw/Ou/eMOkfj04HvmfwGHTuPSn/sXh7nlGr35Ka6ylBa9nob+W5SwT/lPd+h9ly7UXmSWVgszH66vmvD7o4qUtP/fbNXB8SNJujbZCxJCa9t4em16OFbbpUe/dHZtHU3e9Fqgl9qdTht9r7dOOTGn2T4Ksafc/ctzKA/q+800epyXtvxbwkqpk2KtMwMPmSNqPPrb0OztovX7cJUjHQx3rGs6Nb6wUc0glncdugkL/fTuPrqJ9KRxAa2F5tgQb76CVTzLpOGDrb0OFcW4dzNUDQrz8QevdcPx78elcNW+/r6p5xMyzR93t59xrpwE9Mxxax9DbB+yRfGOlhwZtOapTUZ4QW9PAHwf9JmvsHvx8UG22vvPP14FeCewQ/EfTcfvn/rZ4LfcXg8/PdH+a/64d+VO5/L/Sdg3d0P/eOC17LfcyS62W9G/pdue4T/G5Qfe2Z508Nnp539wmenHveF1r5da1M1879ZylHrrcLPlTeXYNHpKw/zn8vnmXu6baaBv8tmWHw1kbE9Z/k3k1yNSCqk/svnRk4RrgAwtDIraH7WVnHCb1f8OIbV3F9WToNy2wv902q6LdB2c7frZP1hwFMqhjtN9GXWKsjEkUxt2fpyHu1DrnbpIrV7u8U3DfPTZbWAwX2TX42ynULdHB5/tskKELMRZZUERW9k7LlundwBTqI6aDyuuc/z+y8tDJhF9wGnf/ncl2WK3rTliYXUOl2eeeiwe2UsTH2zgOd+6vad/du70jboEJ0ByuCF8/vTYPySZQXhVX5hJraMbhVkKFuRXAT17TFTsFNguhdg5MNMab6a4OpqK4izCrDnsHLJf0dllafdfVxEd9RLyNcgGBgQh0wjXuJzTff/J9m7jT2PhHRf77ZZpv9tnWm1eJfY9rZT60Fw8zeGPrf8x0i4at0/txnpPO/JTdWbzMf77PPBL9vBs+zH096f8x9NoA3h/5T8ned5OM5y5Ytk8c7Bh8W/L/cPy7/39ozwRcHD8tzf87994W+TPAPob8Y3DfP/yz44/x/0eBXQ/vuJfPMibmir5r7r5RGrjfJ/0+QRvDewbu3tJ+a566f//+a914XPDB5/EOuH871Yrn/x/z/jSV1ADBL/8z93Pt8/pPHKwTfpXy5d3juvaR9987BY6WXe653bvePz2/qyZ/znfekXa6inVJHn8h12RZbbNFtvfXW6xWjWhtMt/NLg38J3j/4qKaKvSB0/7/nRrgAAUbUqDphGvivaejTghj81+lg/5frqnSyszW6N0bnIPL7XF+dNPbG7KEFecDg7w5y1LDTi+eZ00VW5ven2qDAQEUH/XPwOsnDfyc/f0+HxOAP1dFz7zHBW+e5v+SZ4zG4d3PvA43BlYmovE/K83sDV+i98v63vJ//L5HfH27PXTnffk1L40Z55om+FRqD38P9XJ+W567nft55w5IaA13+PpZ63BdThj5trsaWI8b/Iv/vl3tf8E7oA0O/W37znesHj2/fvVPwuDbIPjpp3jmI2V+c3zfwbr75pm233dZ6Ouv4u8PYy5YvXz4JkjzWqvtpwOD+14bBl+d76udBwUdKI9cX+n8YvOEIFxBIZ7EMVk2uc3MXx4StI+yajrAinW8uOPvafIA12/c5yoiCyvKuE7EWO99LpyO6C6roOYcZENPRLLx763Bz9ZwxIrrnic/EcqLm8iDG2WJJtfQTf1meged3XVqtymhLVsq0dzr0UL6VS9eIz7vkmYHe1v383wU3a7SDxojsewc395x6WrJmU4e8DiK6mXsQ0VnqIXrPuWZdD+yeq7omolMPpkV0EsX2GzURXRtsWeEyO+6448X33nvvXXbdddfL7rDDDpcIbp4ZfKuUact8S/vNtsFqaGWGxH9l3Trv7L60ShS84Bw6YbVgUKVmPzHCvwJgoCngcnla8I1pUC6bX83/p6ShN9IZzNzzZe6Z73Lx/Gap2yyl4bvWpVeG/mKuzvTipfaRUoMK8u5iWf7frsZh4275rUk16L1kUq3gTi55XDrjd4O3SP7uGSQCPyj//XueOT30U5dUI9u3g68JMrJ9K9cPYrA8/6XgF/P+qlw/vFE1gBkA3pDnTltSZ+Nn5t53gtcKPiTPSO+I3Bc/ThoPDx7imfx+TtK9RPL7zeAbQhvIHNIgWuyqXD8e/EJotgXl/HresZzHcCi9w4JPaWW6VfABrUwPDDoe6bQ894S0wWG5vi//PWqPPfa4Yhj6HsH7hcF53X0k+Ml81+DWq1Hrg4HJl9aBzGD08HxXOVjUbx36By1v40x+AYHBC+ujpe7I+nPwO2lY67Z9Jzib4LA+37UFUxo2hfDDlsZfSnUeMXN7hpcZ495PvdPVo4GGZTLM8F50rg4VfBE617sG+YGjn5j/jmjPvDp4zUZ/ODh4lp0+V2dPevMfl1T9+IftfZb6YZmMiym7wLCExZEHfb9gv2yV/5/Z1fjmaM4ydpcpx6fy23KfTSynRqTm0vqtMNQ/w6DW+DkH/WVSl+747Hv/8CDfeGncJSi4hPuPDfLtVwf0496RJ3k/ZtWqVZblMOq9d9llF8clSfunKQ9j5Jnq0NrUIOC5vMsHX3qW/npvwK76tq9mcDjCvy5QrK3d2nwBdMqViG4Dy1/zALHCbK8cwNKXGbunG3PrPOhBLEdzcEG7x88bzQ+cUwk1giOLdWuWeGeCcRzhjMLxhTsoxxMqh3PGOJMs9cxcXQNnXefMgil6m8Nc3YHV52NSHV04w+wYxPREXtZnNKu/b3Ga6QNE5vtm7kG96B1r0PnuJTF0ROdNw+QXD+6PjjRk7Z1PgeOGe0eXSRWHV0yqE5FDE5SPEwqVwYrAQXnvotG7t4u+fdUVK1Zc/sADD7xYmPzKwcuvXLlyh4juB3kuuJH8tzJMVf0Zwf8NV03qkicnHSsXh6YM6kddL2l4pt8b4TwGHW8KbBSx/nx0qQz9mlJFaWAP87xh5rtmbSK3/dP2SfPTFgrJrM1Z5dhS90vbjvncvOsc7/el81APeHdx5Xx/V0X3F3XVrdQSGtfRU7p6OCAf9A8Hr593uHYST4VXvm67/6iubvrw/LPyP1fVk7vqALMy+J7gu4J2YTlL7ORJ9ez67/YckfdRk+okc1Dwjo3m4umklZO66s9tgwiV4phSB68PBZ9tFu3qmvMTdtppJ3r4HYP32X777TGQ3WZcWA18T/N+VzeBiEkn7zbz3LGrIvedgjbbeOZhGTCunPffmm89EpNnkHgk9SHfNSC+Jfi2fJc9g5eeQcsgVzYE/p/GfOMWwY+F/q/gYcHPTep5awakM5UMRjj/AP2YWPfaUiOSfC5oEwfgXXZ2AxI8s9TvCo98VKkbRGyRPLjdJ2oTYYmBHFrsDvtns6qbrf7a/jNjswt4xwaRE9t9Hb73ZAt9j+Ax6EllnKFMNm70m02CzgS3E83zorhyi3X/r6ENOj9r//ElJz6jr1LqVlO0M8AdQIA2sNiKiqZ2GCR9y8YU+8ypIP+bvBgwPfMmBrHQGOOe2223HWmEDt5b0UvdR/73rpbJAOsdwRl6T7bg40MbHOX1FfmGlQhlfUukAd6EnIPevPnmm5O+nIVOxCcJOAPt0EmVcCS/QZhibni/UvNhABS3XXrfwOCYm2i/8QbW3Ec4/4CoJWZYMw/QSUQ4PaewMohBlpfqT07/ZTwDBhJr3QBD2OChUxFT+ZzraDZ6XBldKmMbGID901xjHb3LWGXLpeN7DQR2f60Kbh/E2NxoSQC2ctKNqQqeF6WFlMAecGhXfdylZYsoK78Z2ekp6D1DH9KesWXUu3zwWRsx1+CiSxde2Wi73pyRRkQ/eKuttrrirrvuuv0222xzYKN3yEB2UBjl6mESPvXqw2YWZ6/Jsx1fVhGkTRqxxEbKYHe4VN7hQ3696N1XiEi+XdK4dvCq+eZWuX+Dhr5LbfEs0bplbcPQ1XaADJo8Aq/SBot/z/VaSWcJ5macw+AMryOc/wCT2OLI2GNWMxPesVSR/DFTz50p6AxTIAADSWDYI/7CUju+2UtABfuczdTuC2uESRjKHtlVeGHwGa2DMV4dH9SDqAwvL1Wk5wNOwpD3I7tqgcf8RFoBHGwjxYRokUZtWEH/Z1cZhq83kdggJkKLvOyY/54efEVX93nbDopemSsRXNrEezPny7o6GCjjS4M36qo7LVrwB3vR0UR74Z9Z/Y+L+CxG2n3CJA/IDG4ftjK/IPekQcp5ZX4bXH1DHg8tdQuuvDvZ5TDPBO/aBgT5eGTe78JokzAazz4edWutXw84X+jq4Opqw8vjg06aoZ5YWXhivu845ncnvdfmuiUmPwfG1xEWCXQWFlOd6IhSLb3TkUZfPzyowc8CiK7iffuaHx0kZhNlndDhPnHWMhkRlh6OMdwngpMkiIH2Z1tPdh+amb/XaDM5ewFagASMh753njEIoA0QgwWYPjqIz1SPIYQSC/0gokN68K8bjclObbTB6R2NFiCh32xSqn3hno2mdgxqALXD7I+mdhhQerUjaBecaK2nRpQ2ONlS+tOUl2QxlImILkor+m7BJzaaqC4MlRUHzE9k/k2QjrxaHx6WuzD5wNRnhbl9Z4rBDZREcqsS/WaaXG091Xf+2ZyCth2W2UY4fwFmYg3eps0g9m/r/ERYoY/6pRdwFhkc8+oMgDRgABmsMVfrqp4NMMQQqggDCsQAHLtLPJWupSczGKBCkDj0VnkT7JDoTbwXiGFFVw1iZnQMujLXo4KeZUM4MnjdUoMuouVxy1IjovquLa83KfWctM1yvWapARjdl4Y6Qa9o70qb6uEdhj+VhCYtAPk+WDnaDHh46jnkhJRxo2XLlhGfGeluGeYQQIOqwBipfujNR5Qa0UVZlMmecX4Jt/fd4KpSDZb/Lg2I0RYCWp4hEd1aOBHdPoC7B29OXE9Z7h48Knnn+LPBtfYRziXQcFOgsxNTWYOJz2Y/nX1eMPOtB5b6LQYlIuwjShWlGdDMQnaZYZojg/ZGG+69Iw8APaT9kOC9G83I86hGE+dJBAYgDh+P7apF/fBSDyIws9N7Hx+kR5tFn5ArxpQvMyDR1/LfE0pVJTDTMaWuHmB8+fBd+SUBPKarujy1wnMs4gxi8oTBfEtZ3ROmyTq8QQmn+SbGnAseE8a4X5iAzeBBwYeEKTyPQY9OfcjHkaWmh6kNFMpEmjJb9mUq1YahHAaCRYXG4NNoKfLpwQckv/sGn57yHJfrMrP+IEGMcP4BDEn0E0nE7E3cIzKfHRjEWYYsVmtiOaagV7oPzOSnlLo01u/JLtVjbWWpZ2qJAmNQcJ8YiuGI8X5jpO+jG+N+tNEGk94ppFQ1gO0AzXpvQEC/tauRUdF820kK6J93dXZEQ4OcOkAbLL7aaBLAOxttMBxEdOKyAQMtwowZGE1E37/RvNkGFeSPYY6VpZVpUh1gqB2s4kR0W2P/VuoAQT3qreilMrTVB6L6oBJQGRYdZhjcQClPXw0z9yJ66D9N6lr97IA/wvkAdEgzIlGZWPnYUkXdHs5igzHOmckcJIDJ6afE85WlDh6ilJi1b1aqYQxgjkMafUSp+QB3KFUMBZjUt4DvPLjUI5EsVwn4L71rlTWRTDEvQ9ohpZaJocxAQ7Q9OnhE/jNYuM8LjZpCYvBdAwqmVQ4zuPSk4b68eYbxTDoGE0BMx3QGB7O2+4e0/xgC6bCcQh4UvEuQRf5ewfuFVg5lFe5JXbFRGBDlj2QjbQOFEM6McaQTA5K8q8cezmI7nWUYGLyrjkXDDE5cf0zwYZMaPHLR8zHCemCm4hlwdBy6Nr0S82D0swo6M2bAHJgME5ltzMCYDHNoeLqvNC1HyQidk1gN6LFmStAvYbWOQgc3Mw/P+BYwC2N8yp48c2bBfNK7a6lMjKmJvWZEM6Q80qUxDUYk+hKH0b7rfYMIcZw+TvS19oxmwDMIodkVjipV515Zms5eaoRWxsRhSRGNCUHv3KNMYYKjgjdvjMJ2QHQ34MnPHUJTO9QBpxZ1KD15Mvv7Hiu+pUNlUr5BtZlt30WBgcmnmF0Iqwd01flG3VJ39AX1OcK5CTMdgPhMxHtUqbP3/5VqkT2rQH8cxFnr3KKqEj110t5vu5zRio4p3Gdx9s6fgu9O/uiy7ltP3SUXQQn91lmIpmidiDiLNih8pNEGDyK65+jt7AjuP6tMieilDg5oIjrmQf+srLF2QzOzMvmWwe8r7f7BZY2IznL99EYbJAYR3T2iPNqz+6BTtm91TUS3xzr0yvYMml3gh42WHhFd2qQFji5oZaB/e8fVIGpvwFtKg3ODwcEMg/d1mOtvuqkjn0u1dYxwbsJMByASnlBqJzILHF+qcaeHs9BZrEvbdMF9dM9SR+8XdHWGOSL4nK46a9DVdH4xyHX6pwZtkiAae/+2G220kW8RZ2+y2Wabmel9ywAE6NR0XrMd8dUS29aldnS2A7O2gcP6OlGWNd6aOkY0iLy41FnRLIj2be94BsOQJujP1tvNwu69sN0fysQf3uz9vFIZ1yBmADHQWAVQPmlt2t4nfbA4yff98i5ruR1mNovwY39qrpbxOLQcXeqApz4MGGgDgjI9t1SjGhVE2sqGmZTbwNLDWWizcwzSaihIhzX7Z3V1oJJX9UZiG+HchJkOoMPQ3zCljo52bz5AjLRsBOmMOpylIDT9kJhufze90TIX0Zb4fmhQZ6Zzes4sDdDyAGymsMRlhhC8QEcGmNYgAYjdOjvAsAYoTGX92gytc2EODEbPN+sTgZWPWE7cP6RUyQBtkKNDe97SFakE8xoclNWM7D6a6E+94L0m/yza7lNVzGbyMYD6AMpnw4k24F1niRBNbOdJ5xnlmy4TT7Y+amypaVg+k961Uy+WAQ0EpBZ10UP7zrkCbfZ2pX5x+uFFuE1QP2BvUIfnap5GWBuIfkQps6HZA/3K4c8zaZhBlCY6YpoflSqWY4RPt/8wGjWAGGkmZwF2n95P3PxdqbMR5hrEWaIxUfUTpTKl+57DlKzpfptdv91ozHZSo6V3QqnWZ0azYxttVh3E5zeXqk/L62fKmrPJ5J9o7D5VxYz88/bfFUq16ruP0d7e7rMk+/YfSl3CM+u6Txrx3Z+W6qSiTO6fWuoAJI3fpn4FhHCfeMs1d1A7MPegdhjQXl5qPd+u1OU5ao6ykcA888bS4EzabEFhSky3Q4/ziyW/YcWgVzvkZ8ARzn0gCr+v1NlWZz2lVFG0B423ATAbv7ehGfK1pW76wHBEZ0zH4EW8lobZkFHM0huR04yPoTGiAeLdperoBg6DwtNKnV3tp35tV4/84SpqmcvsT+TFaBj/P0vd1CFthsIPlbq8hCFOLJW5zXTK51ki9MmliuMYWV7ZHpTDdyw7Eb+HMmFQ4rLvmi2PafQhpRq/5F0dKhP6iPaO5a0HlFqm95Sa3opS6+M1XZ2Nlcc7JA6DnbTV7TGllo/IK/9vK1UkNzjRuUkdBlP3V7fZuclIUwxul5pddQyHewTfFnxXV89FW83g52beLsyACQ4uVXQ0Ew7i7/kWdIzWkfadNNG9q9Z3TAQwBEZlUf+XgNmO3zq/2d0MCHYsdZABBjRi+gCCUWwa/V15Ob9QRc4TmGLyAW3xPSRoxxoVjYplI1Afjmtk8kUAFT8FJ5YqQtGdzRjoo6cfOD+BDjF4RrE85xanEBtBvoYulSnMjsTnYTntfA+zzN06/ndKLRNj3QcbTQoa2kn5qAK/L21dPfiLUo2j5xkMzK2NchX62dbUv01qUI1hZaDfBTgy+CLATKWyMH+5VHHSYXZiod11eOasNIBGnX5vQ/R80TftRtpss836WG/Q7xYr7KPBL3KHzLOvC34haKajB3+uqwcJzhtm056+tz76nKLyDT7itlUOO68wR/5/e1fLZDMNcf7zpRoeqR10clKKdvtoWSOif7hMDdDSOC9gGISDu0W6EGPui5Ma9ea9ydNXurpuf57l78IEq4I2KnDSoL9a++Vffb6qfIyNAYLLgg5WwBiCBoqgilEweZ/vUh1MhiUZS1KcQ87X4vqwbzrX3cLgO7fdXkJBDXor67nVhtlXZ4EeP1jqz1NoDA4uAic1jJWVA3v6+yi58yjPCOcQGGX+mIo2AzyTFberYYxWzzDnNgxpD2hGa8y91bJly07OldENM/wyKOKJ0L1OwvxbV9erxR5XDgYwFnqzmqWa8wXMlg8zY/Dg1inbH9o2S2GbvxzG+EfKJpjD21uZ2EweF/x2K5/3iesMlgyJPy7VqNnDedF+AzQGd4KM3WYOMjRgCdD596C95KvrYITFA8tg9CLW2Cemsi3XPHSo+POig0x3funrKJg8M/ZyzJ3ri8IMjub5etApIAIRfiDP/zR4kVIdVZxTRh/njspbTec/38B0GYfy2UGW8n03+KOUy0z+0fwnvJI1bo4jPwuy7Bu0hItmUKOHf6vU3XbXDH6j1COKpw1daxI+F2Fg8MbcfBkwOFH9l5MaNHKtehhhAaBVJM+vqvhVcY5ex0tsYwzS1SWo86zSk64DEIij/UaFdAR7ih0kgMmdibWs6eEOL9i8ibPDe7Of8x+/8NU/p2j3h3rgAUdNWRToasaoCa7O5+UAQq3o1YuUwYClTLsGd0KnyLZakk4GJrCddObLZwBqSv/d4MZ5v4+Pdh4yuait8iAQow3v+wVFYTUoa2NRZvW786y/XSBgpoE/UKooZ1mJRfbbpXp1nWcj6kz+eND9Knl4Q1fPpf51GOCjwZXp+L8Ic38jV4f8OfnTDE63s54uXjoPMiLqV0p18aR6KCOD2y1LjQxjLdo69Q9KXYs+tFQnFmv4PSwCQ5AseKZZwuL08stSI6CKy/bLMMCpKcuKzN6nB50QuuPSenaZGZwebs1fm5FKrN0rh+/cttTjlInovPe+k+eenG8K+ywC7RMGBjcQnhcg7YZ2DX4p+ItJndGdCPvrrm4gOtf73AUKZjrsJyc1+D14RanLLBxbejgfVLSADfRN0TqJd/TsLwb3oHOn8/88nd8xPc4MswRD4uAtZ2mMOyRxVvkYDQGnFc4smMH9h5XK+H8q1Qfduqz7LNM9LEIdGGwwpPXrPj15Doqb/reUwekkznRzvpvy7tJ0cMuAq/IcBx95ZDnns8+LjwWdY5D7LOvsDPaPvzDvXHKzzTb7bN5/xsBgGP28gCkGF/DRUcnKLhjlR1s92N67GHV+4YEZBmdhZowCNnPodNP+0uc18FNn2bc8ZL+0s7oEJCS27hymdhYYevPc78/Fap1jPtNu7w/dgHOMGR+YGVcMfyxCZ5M31n1Xde4QBIEVtc2eKccuGDBlNHM7iw1D2ojSW85bfqbzPk2TDgbwzUFER+8wVT+LCUO5BpVnzR9rGNzhCMptiUwebUqxDXi6jCOcHZipPP7QXyy1Uz+50fMyQg0NsdCNMTMAyQs/7edP6trpV4Jv1Pkzg38p6GRPZ4ifmPufbe++LPip9j4jFPdPHl82h/A3HzagmOm5rFoz/kKpPtx8zr9Uqo93Dwtdvhk4MGgt2Nr9ykk1PL0/DL1tyqRsnzZw5b/Xd3UdnH2Aq652Mhg/sNQAkcrAzVc5SCeMbF/M84JaOCCBTwNpZlHabAakTW1g0GTjWQ1T6Ysg+6GWL3X+6uDXQnt3sfN3wYaZysM8xNmDy5qIoEcNf84w27kCM/nDgP8I6gx2YxHDvxmkv/6TCBt6qyARm4hnZtTJlYMkgtmtDKws1Yfbfb7ndy11YwYdlojuPvH9sFLrA/OfG3D1Ust3alfj3SnPr1LvnEL6DTShxVf/JrrUTTZsDGiDwwsbTa0ymPnWI0qtN/dfWupg5v4w6C02DPXJ9rHWLD7F4GZqnnbajLpikPLO+WYJ818WZhjIbMbohBn4N9NPOYOcX2AwSBHhWMadJ2YtmNi6D2Nbo1lgual6x6i0PjVj+v6glwMbN8yOgIPPRaf+W2wgmgptZEAlpfTlm9Rzx3rRvatbageHHRaygQaYfgCrIIMFzT57bUnhvlxZEwJrsUGbHVKqNHSGGaKVR0gqvugOjKBiqHOTzLSKMcLZgSkGV/ncU80IGuNMYWZwMBh8vNRZcrFkKjPqB0s9jocawbrtbDJLRoxNg9h5QqnqBniQ5xvN+PSkUvN3eKm70TCvvL+91DV/xio7xe5d1vivP7FU24TtnWbEM+iTCwQGWPXvLDS2hfdmsHr5pJ40QmwlmnuO+vSG9o7yEX9tpLl9qTvvMDDJZNi9pnzqiuFNGtSUJ5TK5CeVKhIvVpmk/aFS+1Y/2Mz0m9Uz+RRY3fAOKU25rOtvO/3ACPMABhuzwzCKljWbGOald88Anda73y7rGKnPLsjfFFAXpMGqTc/s0+vaIQG5/qWrMc+J1f5j3MGg1I1hJnOfSI/Rib1ESMzs/nGlLsURYQ0Qh7b7ny2V2T9W6qCwPongnMK1Si2HkE29iM57LXWw86SubviP8Y9O+7dSZ7gT3S91LzoRHH2bskYFObZUxkdTOwxyaIMxaUhdUVsWrEwzbaY+padMvT+CfscYOg1TDO5lTjnagIhOCqG6cLUdYb7QrLFrMXmpBhGVOi2uzheIiRpTJ10sWFHq7raDS3U+MQsL9i//hwSv0MphFjbqA5tMVjYamBEGmBZTzXpDJ6cHSktnE0WF2Ey0pef6dg9TnXKhwCzlQAJlSbNMRGQZyid//UaMsrZITpUY8sQ5ybZeDxF11ZNvMipeo1TVQxlFmVEW93m8abMFL0wD56SJ2qIsfX9rqx4bOnABQ+uH4sl7yKC93odHmIGhonPdMtcXB1ltHR97bNC5VcS4+XRgnYaoSEw045hBHj38OY/3zwmYhRmVGMbMagI7PK79d0ypUWGA2cv6NtBxHtpo4p9ZHLPw4/a+GQ0TiA3HUEWffX5oMdJ2L9XV9Uk6nbItdPm6qovq1Dz1xA2XtkMN3H9sWaNq3L2sOQeOtZzqQC8nlj+rVGnDIEV9odZoz+eX6rZqRlRvnHqUT5l8d5BwFgSGusnVQZD61H+mPM4151L84lyXYfJ1ONl48ehS+5KBi3RFhVjMieOCBVMMbm11EP0cunc6urT90uvqwDP3BtH2Q6WKfsSq73rMn+t6fwHB7CRt6WFKFme/zWLyQYTlcsof232zgAgnPMUES8DQ7mMQe6bRBgiMjSaKYw60LaZmRrSIoMN6clkoaN8jldgPbbC111u7/KmrEV36diqVkb9TqmhtcDq51NBOBqdXtGdYzakbaNc7Nvo1papfaCsDDKpo3npWHBYE1iWipwxfy/1D0fbrp60cfbUuJxuVOrSZfsgeoqz3W/3AAtb7BRKmGJzxRmD9+3b1ZMxbB9Gr5tmBzdqYHHMTfRlxMEwP83j/nAAPsLuU6korIZ2aPg3MZAOtExPlgdnrOo1mUfacdzGNUzgxEiYTg5xIbIC4fWgi7Rapp7vnesSkOmbMduRzDF3VUfmM2zJpFhc//KatLZRzKNPBpdo8AIPocN+gwNvPILey1DoR78xAQJKxBu6/O5XaZspNGqCzLxawjzjg8Rapr53S7+4bxr7zXN1Nti4GB1yHvUNqunap/cqgPMJ8YOicweWTesrEcZPq9qixH9vV2WM+DErPNUOY9TTGsaWuKZ8bwKr9yFI7K2OaWXhIW2dWFsAybiAAmN1sBujfdytVvF8RvE8rN2Z4YFfDBxlEHpbrrVI/26UzOg/s/sElg+1ioaExM7Q2/IiuBdlwLTW/wKw4lA8DGGTp09QOore2IJY/pNQBAJMJ2mEN3ECs3jC1Qe5RpQaBZHchqhP9DXILBeqZ2mawstR3bPAhkxqyaTWDz/Q17XVcV0MrG7z0MTaDEeYDUwxurZgoOzgYsM4SjSytzFZ6DzP37lGq+GRph4HOu0TmcwPMYn8Onl7qUpe0B8vyIM4Sy4mzRHbGtg+2+wam5zaa0Y4HGFoHN3ug2RZ6ET1l5lHWi+icaUJzhV3NjAsJwze7JqJvsskm0jeYDWUyw7N6ozHuRxutPl7Z6KPKmvPW6O53bvRry5rDHIjoyqRuflzqgOC+39NGvHMK+pJ24byjj/VqR+pw62kGnxksv9nekVciunyNIvp8YYrBzeBmb/uEdRYd/Wmlit7zqcjLl2rMYrwh3j6xTDXEIsPKUhnynmaDUo1tZi9gg8VgWDNTmckAQ83wDMMamhWa6M7zi5Wa4enoru5k4jBDohEJVISYJ0esfNSkwmIzuBn8cUHShN/aZigHBn5Eo81wZmTitkFWPSiPZbNjuxqeyiyK4ZXJ7K7eSDlmcOvh6oG1He05Us1CwT6lHYCYtHdLvT0l9XhMroJBnoHBW1nl5+ldPRCDdCi/4ww+X1CZTcTcfFLPv7pb0AYOot89usrs8+m8OtIdSnVmoK8eWapuR88jNupEay94LhwQKY8MHr506VJWZstLh7cOQmwlmgOz1LCur8P4b4CBxuQ37upMib5F6AO66jF2m9TNNUPb8XT7oCUfsyhxWV2REuZTV/OCln9IB7/dVJnMZkM5zITsB4AITj9Xz2Z9ARYx7qpS7SHKbEY20GESbXNEqYMBUfyorkbtwdRHNqTyLBRoJ2lcP3UnPNOd0veOCG48q4NPld2pMLa1Wq1Q1iPLeLzR/GGKwW3S6EU/FRnsrZddO8RPZc/CzD16ofd5SbHievfrpXY69/9Q6syyGHBoqWmcmrJYFvqlFYHQ9GaiOxFPR/92e87MNYjoJI9BRNfxLfOhzWCYAv0/Kcs1Gs2KbkZUPtFTGOD6eguuap2yLARMfUuZfP93pUpHvSpVqnGR2sFyrtN/uN1XH4OIbmClt6JJIOwU6Gkr+ifKmuOYifyMWEOZqDM9LEC5BjXny12VJnrnnbTTVtMM3vrjgPdIv3QiqXK/vtT9+UcOH/TMCBuAQTRSyXNtHXxS44hbOnpLVx075tO4vMl0qsH4Y5nmmFINOa8q9RCABVt+mQFqhEMBjt5ss82sHT84ZXhIaNFNnhx8Ycs/8dW6r56ko1sPNuiYEblPEl8tTb2ktCORu+rqiklIMi/vqrFrZfBVQaLjitw/IdeXBm3+mE9dzQumvmNwUofPKnWN+smlLu0BZZJ3UhNJ6cWlDmAGq+ODwhCz/MufM91IMS8rVbQnMr+8VH8ADKT9iNBmWvd9ay0j2wKUTdrUnj31tTD2C3IVdWetGXyKwRk4SUrKJ8/PLvN0nx6hrMXgLJmHB28yqadOWJ4RMqh35JgHGOmJuYP4ZLRmwOJ1RFS/eqneYAsKA0PpIBnpl2y77bYGqkuFuS+lXJtssgld2pKQx3VoMzYw8AwdBZObiYGOxGNM2enzB3dVTBQwwplgwz5lZ6cdPKmB+q/bVbFzwdfEwfC94dtBA/DgeUfsHjz19ih1+csL1Cz+DLZfsh8c0tXlT2eVYRrtpEzEcyqIdnb+mQGOiI/muacODBACLpytKKfD8111rVWHIqaK3KLOrpN2sq13LQYHA5MPaQZXtb+00QjzgSkGt+mfGM0yLFSORqdvir82+1oPM/dZZ1ld31GaiF7q2Vw8kNz/dWmzwfq+d3YhnaMLQ0+22GKLTXLtO0Hy/+Ctttpqd5bnUsVCuieLrN869/sbTa8bRPQjgw9uNGMOqzqaxdkA5Tsi3Qwi+g9TbxhencGVQ6dcSGj1ZVcZBu2aQxL1h41AHfuN0U9q9KF5zAzclyn0o9G5EtXv0GjhnSyVeeYjXVM1cv1OVwcP9/nzo6kG/ttfXlp+5gXD8+0dhk3qhP35vYie+7+eayL6rE86GBi8Aeck+TrCD/cXuq4vcDDF4NsE3xn8SCpNDDAdykjbHx8zDzBLnxJ8tE4RPDnIvRLDnRx8U9cCAi5Go4S5J8uWLVtKRM/3bx285zbbbLNtV10jxfUyuDjKl+5t+Uyn/0CpjEGtcN9sbaY6satGrYNzD80/QPSYk3K1u4t32Sm5vjb15UigD6RzfiDXHdc1E50TUF8Nt5vUIITD/WcG/SC2s3vQxw1KIseKiPKAVg6zNaelk9rVoK1tWOQN4gaFp3RVSnFffRlIPpi03pkr//G3hjYIrOzOAoMPz04h9eCUXJ+fOhKG6iOpr7cGxc5bHfd9Gmb6yrGlthnpYjWDL0Z/usCAztg6pdC1YlMfGrQuaYdDj7PvTAFdz95de5OHM6V0FOIeURGjs2oTeXtxeFKdGkRA3dB3zzKEubt0ELO4b18sHWWfVrZ9kqb9zzqEfBJve4Zp+UMTY9HKu2nwEkHeagI1XnpSd3ChL5dv+q5lnSvk25cJLk/nvFLSvEpw0/X4VJ8jGDrxFJK2iN3yzn9BnQ+0iKSeMSBgIuUhDotSus2keiweMKll4oVHAhnet6f+Yt5P+S6Xcvgt4unlcr1ycBP/wTOD9j3PGiy0/96h+dT7zqXn6qx91dTXQblulbY7aNNNN71K2m3TWSaf+h5PPCoE9Ul7KEM/6M0nTxdKGESjXLfL9Ve5JZjfZdc3E6lIld2ApZmY9exgL87mf4Y5M5/7RDEiOvrHrYGvkO9eKUyx8QZ2EJ1lcKpJKws9lBj31XQaA9APuhq0j+fUqf4LrRO/u9FEbwZAtGVC68vK8bigNX30K4O9FT15N+v0Inqu3056l046f2MNTnlWmIXgQkPr4JAc+ykrHl31rvtSqXk3QL0PnbxePXg8Ovk9MvTR7f7RwSPb/eNDE5nd/1De7dspND/xvfK/gxV+nzLumSuffe/sp0+sq19Mg/8nrZ8Eecb1/SJIylNvnw7ytPtnyvGj4OVb/XHm2Ws4fgpo06Hspe7T/3tX3XYPzn9PzTevOZ88XSgBY0/hNqnozwe/m4ref7g/C0PDNeBwYW8vhw9r0N9JpT8v9OVT4afl+qagGdQzAvRvlWcuke9fMrhkIRnBYNHytqLUuGTEaYa0jwRPDb1j0n9H8ndarnvk9wtz/WauJA0dX7inm+T/OyWP3871fsEbyHuuT85/B+X+6cHX5jcD3mmhT0y6pIWvpUN+LeXZxewz4ELC0Mm7GnqKmPqJSZ2l39HKIdoLpv1G8mdWfELydnquNw7ewzO53jPXGyuH/0NfKfitvPPSSW0ndfDu/LdL3hV2+vNLawDLTwYNZntiOLghwGyTNdLGEUF1/ry8d1i+89185y25Xj74ndTZx1JXjKJfDZ6Welw5zeD64NS3XhQ8Pd+6ZZA09aD8Pkh6cIQp0EiNibeAqWxB9S+TCr9Crn6vd4/uwOBd1dXo6TunggXgPyC4a3BpEBPsMVcBvV/SWaKTJI29gwvK4FMMINM80+jJfu8V1Hl1AlFJ924dgqMFsVHH2aLdnyypR+zuvaQelsABY9+g882S9aUXD64Mkj4MUvumDETKSwcPCG4eXBlUvk0XsnwDtLpXplUt78TUla1MPOz2SF67oB1aewVZqB2awFbgCtCCN3qnj0bb3ldup6XoF3ulfHu2fnDR/L5UrnPtv9lsnQGmmHKX4IHSaWleTj3mW0JaX67V25Zh6MsGD9y0wmoGB/LW8kqyIAGyF4kqe8lW5pHBp0FlwUY/JBV9P42ZSv1+dNl/pMIx+joZXKMNzFTqOvfvU7lPyndu3sSs1y+p50z9KXhKkBfSH3PPEUL75du/zHN/zvdXSmMRmIBH3U+DXyt12ei0oGCFjqj9TAvGeInk5y1BftDE2ae3PB6RZx7YynFs6FsG/xx8SX4f5n7y+0EdM/iX0KemPPvnm79L3f3cTJTrt4Oeu/z66vAcAss5C7pjlwygn25lInW9I/in4NWT7vPb+WW3CT5cWXN9WO7ftpWvn1GDyk0KMHj9Kfc/l+suy5cv/2vK8ZP83jHX70nDwKw8Z1YmzDbF4HcNSuO1+e5123c+HLxy8K/Bb2DuzN6/Sr7+EHqPaQaXlr5qUAn9llam2+XeE5POn4OPHxh86NMXelARDY3oD0iF3TuVt8cm9cSMP2Fwz62rIWcYnK/zb1K5T8x3/j2NhRleHfrKuffb4Id0nPz+XfArG9UZ78dpvF8vIoNbAvtJqQyO2b+evDolgy75yaT3R8yQ/Lyx5fEawacF5bFn8JRfOY4O/kfu/T7XFwYPUzd5/70Y3HeC/4vBMTdmCH3JIFEdo1x2PswwX2j1DawAYPDv5p7Z91NJ4w/J56WCb1PXSyuDP7eV47YYu5XjYcHbtPvPyb1DPZ9vvCUoAMPv8t6n8t+Oeea3wR/k93atX/wxtFn9TGdwzDbD4Or2VcHrSjv1c3K+ww7zh9Bfyz2z90/D5L8+EwZ/kz6W37cNYmyTy2OlBz03QlmzNJYKyWVul1x3TgWyPhPD6EbzFdFZbS8zqfot0RbjONbWd1mid5+r505h8r3y/Um+2evgaailmHt9aZwD0Mr8yKkPk+C+ycfFWplXzK0xFDnkbt+WP8Y/1nHnYy2fq5byLZZUcX0Q0Ym61Izdk+eNcr1Y8n/R4EbpoBcPXiL0ZsqX/y4b7KOUnBkznE1wTJHlQAy0x6Qe+qAc2pIoDIjD6E2DVBC0MimH57dt9UB3H8TcVcEdllSG6l1UG0MT2/fVfsrj/zODgcHlaVJtMlSg1SJ6kD3GIMhIuWXq74Aw9eWCm2HuTaZsGC0/rr2InqujhqmCxHUbgkYRfRpaY/adPGhkfclcDTX82aDjcTDqOjunRpsCftvf6aqR6nDv5jtcD+nljoB961zV8b6b/z6R7+2RBmXdPj1Xhpx1prEQ0KQMy1+2dzL47Zl8vF8eJ3UZ7GUtj4dOqsUc/R/Buzf6wXPVq+87uT5jSVU7HKnz+uT5UsHvJ/8fCe6TzvgtM3dos+An8ox6oB8ueMczaOXCYg55op3Y8msZ7FUtv4cE7dRC3zx430bfJ3izRhNxlUn7vWJS/R6+HXxf7l0k5WCU+8JcXTJ1yIKyGwDmVaZW//DI4Hfz3RflvWvmm98Pvi31d0V0rh9PvV0q9ffNMPa3c3X22mrJTv+Q3qQOGIyB3w/eIvf0OfQj23/zyteFAoYKCdqy994gEU2n/1Eagz53WZWFyWdBo03BY0tdouGTbSuf5RAOLXaUuf+p/L4EujXsvmnMP6QB/d7d9+VjoUEeW/ms+/5qrp7dZRb/Sql55F//HnRXd2jxT0eLaMO3G/34SVtSmtRlsmFJ6eTku9+YsbRa1englnfomZj6e0Ma82WGswJdDfhIheJXYPfaN0pNz2pAv4Em1+vl90sbfYfQgyfbMaFv3+iXBPsy5co5pt8Dnv+tOOyWfDsL7deTuo5tj7jnhhjtNTNnAl1lcBFY+n4RtOtN//pkkAehfsCqzl7x54jo9PO9MPfA4EMfgflWvx8819vn99Pbd58y/L/Qdf0vC0OFTNY4dej8xDjnMRvVBWCcT4XZoMA7SSfwLUH1iI+Al9Rg6WVFHxxP6IqXCT3xX3t29rvnCIa8tzQ4a7DiOpbW+jfpAr1n0rURw7G08izvZnwOMO7bBspBxPvEQQ461JG9llTr+mWaqOnI4gNy4fRCpOcY4zBEjjLzZob5groKkl9xgIq7VMuvo505gsij7aV80dG2tiqT9nDlGILmocYJyfsr2ne5oqoXtLoaHIE4Cqmf+QaYNDPQvXyfc5Hdibwj7XG4qvTnqopwFYNlkEX9oOBVgpvPSnZDmvIUPLirquGqRrvOJ08XHhg63qT6Tn882BvDgu+eq+LYeh0aZiryLqXGB7eLSWV/vKuBCexFdv9FXe1I0nhzvseV84NBbp4a5m155vNlgWNsDeWbq3r0iUGSBG8tO84+KX9ddfX8RFc9o4RismXyRrkeWWqccGGQ+Gork5nPJhXums9J59PxnXr5+sw0LNknJQ11uEee49b5qW6Nl9xUzhYcMNEbS82jOvzvVg57AYRfUg6z5h3bM3fIlcSCFrbpQHTuqQv1g7ZLjpfch0O/B13q/gL/8TFYJ6jvKRDogy/Cw0uN4vKpUiPQYvRPT6qobfDR196u76VOnbf2sdTjrpjbzA1mvmvWVrf29wvL5bvCa/V/zjx74YWBAYJ9KKBUkCUts9xv/M51veLlUJkNeLLZm2wLn+ACvvX2rp0VlqtD7vq9zEnr55OqB9ts4D+i+w/QpcZHWzCYKt9FMhv4vvTNVKej5S94cqMFOHgZutSBqhdngyLbYAy0UNK2j/4peHJ0xWGzCecMjPVX6aRsjhf6WfsP88zW10KDnV7q0GYe23U/Wmp+MdVrGn235KH3OCw1Uuxd0V3d7tqHbMrVe8O+dnVkJQJtL72VCP1Cu+kv64SZcooE5H3bcfv0gvza5WvoF4eg1VeQ4axvp7k1fgr9h2aYdtjDf+dSz3dHP3P4c2TwBipPZeRKjLxerteeq6FrrxFkLOtjZA2j6AZgVanx0HtLZqk7s9zT4M52ZsGWFo+pfvvmpAbvPyjXuVzNNN5fyMB+0wy+tKubLWxOYJDiK48mwtJj0daV7ZiyNdLMtbLUCCcrSo15do18hwWZ2nJYmPvS22+//dZbbbXVlYOX22abbZYHrpq64jpJTDZ4+K5tmbMdf6EBFxxcat6lRy+nV/PVxow2lhBnlcU2TaK4Zbarl3qmm+2th5W6711j+1a/nbar7sZsKdIweKiTNd4nGwaDt1nWVX9As3tQ5UgQ+gZ1ga2AHwJbybVSv/qi0GGrmbVbu/7kzbe4IGszwUhcR5iGYYQM9tFB0zEfnqvln5enI789v/dcH4PPVDjDmigbfLb5Mr+21BC3lqmIw49OQ1lrf32+99+hbWg5IVeBE4QLErTA+3us/uICgDw2tH/5lcE3dlUXNNoTac1Q1vDdN3OZZQRWxBBm9P8pNeSwTi5/95rUM6sZ2x6w00477Zcy3S14ux133JGdgS/+CSmbYBPPa2ksaJnWA/RwW12lR1IifSgHRmfcUg7MTLpCCwd9aKnnmtlBR/pQPuI8pkGbfQWY0JYnlLpL7UWlfpfNZZ2gvqfgiFKPYzbTGhhs8xQXj3HSfeK6IB1vzpUax3agv8iXOlwfg2sz75sUqBu+e9Tw58yzF16YYnBul58M0lP5A/diUmjri7Ov9TBTiU8qVUzSyXorev63iaOPOppG+lpXZ8QhAinf8D4NjVpqBM//KzUo4IKBPDakP/bpBRmJBpWApMFGgNb5X43uaoA/Yqz7z8pvNgbi75snbaOETpiZm0Ryp/wumcEPaM//1SCQa79hp6wJILGYIIDCz0tND+PSSdE3LZXp5Zfa8ZR2X3v1mz9KPW/NDIj2Xt9mpYZs6lW3hgYOqgkRfb22kpl+YSD1rogwbBloW1pv3GibfuQX/atuzUES8nvRof3AzHdPac8ZnIj/aBuFeph59sILUwzOqeM6RPO5KqJbXzwyVzP7OnXwGdChzd5GY5E/b1ZqyF01/W9dtbwaja8zqZZljWBE7/fzlipOYrA+WOFCwdBBumptNsLfJfS2pRqc7PNGE92PKPXMKzaA23YtwB86yKK8Mld7qA/MIGhjxy0zCB6y5557rsjva2XQOni33XbbKWL7DfPfjVNfRFhBD32X6L/YwNR8y1KNaAYzM5u8E4Utm/VlKpVhlYM1fEWpgSyIu+pB3LmDuxoSySBtICMZaEvBK+n5yqSdtyjzAzO1PGFcA4TZliog7duXGiJLAA71RKogMciT/PZx+7p1M7jy+e5FS80/eojQM8IAUwwuprftdocFeQc9bq6K0tbH18ngMxXO8PTkUkP6qPQnlhqFVCNZI7+954OCQNyr0US1o0tdSrEbzayisc8NIIqaXeiiZhYqAobWsVlo2QQMQNb1r1+qqEtkvVXqwgYPPvd32nbbbYnlDwzeMTM4Zjom+Jj8tuRG18c4a4GyD2LnAgMRWh7U531LLROGxjDKRDq6dqnlMKCSLJ5a6sDKgIY2APJcQz+41MFJu2pDqtRxpX7XILJOUL4pIBkwvBok1CmaxKOu0dp9z0ZTK6gH+oH02QhWw8x3zdzeMSBTpdCkghGmYYrBrUta1jo6tN1Xf83fRGnr4evskDMVrlFYWolLZoJBFOsdQYJEdKMz2jleZgYir98alc/4X8oijcIzebWk1IuzXdVDe3E29H8Eiatonnn/hS5V9BvES0tfjDtEVb7fOq373+OFVWodeJ8KQppR/gV3sl8HaCCd3exICvpSqfnCvHRV9H1KZRw0UZ3Ijn5D8juIzJ8pa0R0baIMaGiwoEahL1nmB6LMeP74Ug/EQNviShJA2ycgz+jfljVpQxPF+uAjpT6jXXrnpFLPD+9hpr0v3NBmaLHQDwteLSi8kbPIHhF68HOefW0WLIeZkem0HCuMzMQonVvDsu6qeAargdb5btfeF/nTmqlZYrGBOGIG4KnGWGSGe2RXbQHEUjQHEFZwM70ymXFEUb1+6sqs/9DgLcLULMBCIh0VGmMxaEEiM1CX50Zvk4ayrCi1zonnymdmJoE8olSm1E7WpK0iYF5tZla3+vHQUgcEbWD2Jk4TxdkjDA5D+Ty31uy6AdAHjilVwrNagTabY16GMmK6PMurbxsYDa4PKRvuC9SRY0v9pvyTLEglI6wLBiaGXfWEIkY/pKteXPMZEc1kxEKziI6GqQ8ulZk04mHtO8Sp/ntdXQvFXEDHYvQhEg6dc5/230KD0YqYqBPLqw6nU/Hc0lkMTpiBSCtemYiklmB0dFFTrQwYjG6a+tLR0UeEpo4oN+yXHdZTb0eUxT9XS70q06quBlSUdzOwGfL+XV2bVyZ5Z4NQD9pPm2Bk9w2+jHeYW5kwu4HRd9dSPWbKybCnfJbW9AsDAnWHhIM2ext40NIYBhffph5IW343ZI/RZt65dKnfVafoEdYHjbmhHUW9mNRtIBb6zL1B9CMu6VzE72kR3VZNzNsfE9veHUQxnesXjcZUnytV1MX0iwFmOOnJI/GwF9FLzffr0cmfGYQdwX0rA4N4+Y6uOWkE/7ervvasyr/o6nq6+75rJloffKLU5+4/3JiHhHRW4dRS88WmMIjopDK6OFp79SJ67ln2Gso0LaL/tKwtopNiqG6+i6lWw0xfeGupzx9d1qg5J5Q1VnsiuoEefXZFdA45nlEGA8J3y+LW578+DIzcVecP65PP66of82wDrguIgf9dqvOBmYHuxdpqFKbv3bF957hujZGN2MiwAjDUM7q6nKXR6L3z1fPOKphdzTAvSHpmEjPUC0qdAUgP7rMmKxObgs6v46GP6urMR9+7X1fP9FJundnsg3GU12y3PpCe9eRDZu6fI5hpI9KJ/HJcOaLRZlMHHsg7kdlgqp7ZTKgnaAOZcjBaaR8zNaPak9p97frSMrMOPpM20V75iMys5Ohbljqjo/kaaFu0WVja8mdZckWp6UmHzWZ9QJz3/lVLXd9nBKQOjLA+aEyHZITSqTXKfEMlE/8872qmZrQx4gOdyUzg+8RdrqJo4rwGAhrfIXiWs9BmnrU60QKC4d0y2W1K7URmEGd/22ii06Mx/rCkpDOuzNWARSLZvtSyHtLVM8ictkEMXl5quYmgyrE+kB6m29AMdZZhpp2IxCQSkoQ8o9kODE7yTv2xN4CUpD2Uw6xqILMCIGQ0hmFLUCb9oVc75gH0fOVTf/s3msVenaLZNKg5BlMqkfqUP2kYUIjf2l99rg8OKfVblt5GOIug07MSE4GIneuEmQ5FFPtDqeKsxvLuu8sakYvIqFHRvym1IQdRzKhNHETrjER09GKK6EN6mO3jjZbv16JTNjPgait6fg8i+tu6NRbnL5RqZ0D7nk48XabVMFNXg3hJtOxh5v+zBTPfGKzoGOVNjbYZY1ClzMirHV26umlDuakrfTm6ehyxAcEz1A6MuE6YSXtQCRgoH9/oF5e1HV0GK7p+cWijf1XmL6IPji7sASOcRdis1POo3lbqEtY6YaZRjyh1p9GdS20knYqhxPt0PCLUpqW6SdrpxPj2slJ3LDHkcPEUZhljENWkbTZdDDATKd8HSxUV5U3ezTz3bbQZHsOjGQlZ/tEP7qpL69tLHQDMIGi2B+u59M+3lBlmmKmrxwTfWdacCrogMJUGgohL1yUN0U3lkVMRKUTdWhIk2srrPbq6PRR9TKlr6eheTA6NYV/bbeC4opl7mE75SATqEM2AZrZGP7LUgRz9tFKlO/QJpTK1vJocNmTHoNp5x8A0wtkA4jNRClNqDI0z36URyxs6FmYFGGKg6YQcRXQKPt3DEThoHleYj2hvVDfLnxswlE9n1tnQxNc9SmVs1nWdnthre6mlL+Jrf5RwVzdpWHaSd4Pj5sOH1wOkIiL9UCeLDeoTM7NvsKmoW+WhSvXt1NUY6wZmYjRgZdcmvSrV1SVDYa9mmXldYNBUvpWlfg9tIGQtp66xdRgA1bNBXH9RtweX9Rwr3dK1SqEPesY3zs06vEAB3YeVmbVURQ7ingqdD5jx/lyqqGag8O7nu3q++LBd1F7fXhSb1DjeP0DnGcz2iVJFwpsOHWqRraKfLjWPjE2vRHf1OJ9evMyVL/ogXpJMBoszcVanRP+kVCMUKYCdgRqwGmYY48RS32FMnP1vMcCMLT0i+aB2KBtjl/KtPpssV6JvbzkPfXpXBzl7ErST8NLrbIuZ/L++1DQeXqq0gmZE69MrVSoa6vDzperTaH3uDIN6qx+gbzDcYXRivndGy/nZACMk8QejGXWJ0pZP1isyz1Qu0ZNeS9Q2cvvOi7u6BfPTwbfNzc1tnU4joubH52o8a5s4Phdk/GEd/WyeO7Q17oIzwcz3Tih1ADKLHNtoDjn2T6OtGzM6of+rq8EK0Md31SEG/a6uHs5oQDwg2Ec8mU5rwFLtFDo249zsfwsCM9+ib3+2VJGZqIy+c1f17s/kyrHHsUUGOgEfzK6CKbwm7SHayqeCH56rkX7O4La8jjJod+U7otRAIGiOQWwXaPvrSYdouwlJNGhMi3nXgvZdYFurfJKSnlnqO1SNmTdGWCfMNNSq4V6p1mzrnhuyavbQ3qVT78/91W9M29W9yP5zwICwR31kzKVrTsjYc65GkhFbiUgsPJKwSSs16qSe3DE7kJxt8J2pshJFiaH2TlMf0PJOnL3ipPrjG5zQ8rb1pJ66KlILvZQ7L0a3r32os9XpBHOrj+piw42NOHZJEel9UxpCR1EJFqx8M0BMvnype8OVy+GJ1AwOTTad9KfGdlUVWb1qos4bQ4s4K358v214msHbe1AbCxclDVs+lY8aYGsuelVX+wBa+T2nnqlo0rxCV+vHvv216rD9BuqQX4Y6HNLot5ROPz/CemBgoK42/A+JZSo1f32yVJHZDLfOytxss8364HjtfcswxKeXdlWs+l3wpNYoInWI5CkW+h9sHQ2TXzQzuWin/hNN5sOeCx6ee724F/q+Q/50snMKOulUeT+cW0RQM+rx6Fzvnd/Wyvtgfl3dLuq+We2G8pfrh4MHy3d+i2Yqbtvq+nFtaei0X2zvKxPVBX1kaGvo6CcM+VmI8k3no1TDpvZQhr5M+e/YroZtUg7SFW829wXeNMgRy7+aeto97TScTSbC6ur8zdThye27JB8GR/SDgkc32kmsd2zpvTFoSdR9IaNIRP8Ifr+rA8/qvA/fn9SgIP3qSq7e7cuU+2IO9M9MDzwjrAPaaK0BnTr5+dBC0RqZiesqX0Osk8G32GKL1Yf+5R3WWiLrcekYDC5fDv5PVy3k35urkTT7M6822WST7+SZlUT14E+W1LO+3hT80Vzd3eZ4WfSdpvI3k/pZh+nOGdoGkh+1Tve0rp71fcf8flCuDko8Jmht/Ie5Pm+uRr358Vw9JMCsLn+fmdRjj/pvDmk0dHyTI3g91x8G2NK72aSGaUY/3HtwIcrnO1Nt9cJcHbxoC+aD81u7PmBSQw4r65NDO6DiB7m+KteVyYMBXmyA3dM+P871tCXtiKN1MfikMq1y3LirTlLq6m7B+7f7j8nzQlGjhea+ZqPfEezrsKsHVPYSxNDHpr5PsjP4eEdknRc2WqTY1XU+wgZgaDw4VyOfiqYp6uiqVDjrN5F5nQwOnMsdhp3bdtttt86MvtfOO++8Q0b/OSdU5JuOufXunrnu2tJxhNBw5hXawQJOSli1pMZjt0d9ZVDk1T4Iv061EDB8q6GyOhZX1NGVk3o0MPHU+WVoV+dfocUDdx6WU1f7QxHmav6ESZb31Xmcqk/nnIlW630hsPacq7HupCEsMbpXW4Z3FwK0U6tze9s5s1AtGMpENd0qaW08V+OeKY9nbX0dDkHQBn046+Bm2sI3/R5gyG/Dvg7navmGOhRrf6hD5VReYbj39NykRqkl/pN8qGHiuS+VZziThsMcPNun0fJKten71fD8CBsAESwbCl37zeCv52p44BPTOX4dFMtrnQy+1VZbdWHsuS233HLjvO/MZ0f93G7rrbc2g5+ed96Z7+xdqs+2IPo6kNNLf9xE9C9kMHDUjtMu3hP8bfC6wZdtVI/jufvAMBr8nMIU88H3LqnH9two+Jzgb1t6j5R28vD40Hdo908IHp77jv/x3lVz/ze5fnluJsT01PedVvixVo5rB82SvnXr4JOXVvH3WM961/Wcgu8MHT/4cunlqgwOBxDj/GGhb6McSe/ZoQ8Oam8Rby/W6v/zU2U4Q76GvDZ8l3LM1cMUiOOOsbp37jsDTR0+OfTtWrlfGdS2jjH6YO45J9yZcV+Z1Jhsqxl26vsOOxRtVX6vm3de1tIQv75/Xn5G2AAsXcPgjg/+sbBNqUyzCz3pT92abZ6zr2LwyTbbbDMXht4k73NP1Ci3yu9Lt80lJ06qhfwPuX59rp58+bM8+5vgPhvXc6noemJjfyj4f6GvH3y193PVWVaP6OcUlHPoPKFPSSdU1psFnbrxl1zvlfvHtLR1zju2+05/uYH7ecfBB1cL/j3oGF6z4hkYPO8vybOf805+Xye/39C+5VytZ7TDHx7jWe+6nlPwjaHTB1+vfGmDO+feMdoy10fm/hEtbWrQocG/hn5Xrpdo5aaDmznPkKfh20MZg+9v5btF8LnSyPV++cYj2reent9HtjQcSnm9lvZHcu8q+d5fgqevj8Hz/lzK8On2joCMbCHecfrMOIPPF3R8lRkkchGJ6OOcOzg+sF6uk8FBmHlu2bJlG+24447bZybfd7fddtt9+fLlm6SBBSbsI8P41qSGTNZBnO8FibCOS7pkcBN0/nN4AIZhZWd4cyzxJLjRpKoNs8mfZRg6T3A4p4xK4MyrK87VpTtipZh0RFXH6qKJtIxNjtthWV6+pB4r7LhdsK7vO8KXyuG7RNghDSKrM9uca+26IIPXAMP3JvVYYasXLP4s2ftjpPy3yaSeBT6I6J4Z9v+rj/70kvbf6u+2/913fpuDH5xTZta/QgbqbcOEjoVG7xSm3C14hfy3Ms8R2dXhcB6a1QeHbRC50UR27XsGBm91qL7VlfzynaC7O1duZPD5whSDq0w6Ef1IfHPbPQ9dH4MT0cPMvQ6e5z3HCPLg/Kb7fa6rsbct1xDDPhTU4J9NOl/Mde9cPxh0BpaTLozO35irp30+K/e+taSKd2K5PXRSLdfrzMdZhaED6cStI1sH/mauZpv7J0+n5/qI4K1z75u5PjN47dw/LejYYYPT6u9Mgzz65pIK71COuTpTPrd969/naiTb03Jl0OvfWYhyDTB8c/hu0O6tr+Z6z9y7Yf7/Wq7HTeoRTtrmJZPK6J5h1OqG98FA5z3tfJugWdrgLSKqE1b/LeV5TGgnq94pzM3KTWJ7VPAWee4bQaHArq4+cn1D0BKZ/uUwibUmkVZ/cC74VnUVZKjkfKSdbjedvxHOBDB4KpDV1xoljyzLFl8pdZnlerPPDxDmLptvvnn/fiqbvzOR8BWh7Rb7ffAToenj7rN+mvX+THScq2us3/PfXGXij6FzFcCQH7v7lq0Om9TzpwRuXDBG0IGGTtK15ZdcWX/5qMvHM4McRNA6ZL/ZJO99UUcfOuE0DJ3UN+eq8a2vw9wTs673LMt/R4V+TrvPgWbByjQNQ121bw+bTY7u6sktaMx5SKM/0NXdc5ZFT+uae6pvDN9qaNmKD/txTnTpqp/+LyLB3Sp10S/9RXJ7QP6z4USUWXp+v0zW1T0HfP3RHG0Obmn/pGvLZFPprB5QAr1HZa7/Nlmz1NifZtKe6fM4wplAGoje5SAEaOsgD7bDSvUf1ku09hmGTBXcOhPHCaMyi3l/yMCkWm6tB6MZ7nR6YipkzWUddVYVMZIjhlmaUwZx0qmfK1rjW6PlM77gzNC+yfGCMXH3SV0PtiRDjJW+mcOJJUReNHEzl7U715CvroL/PUQEFdCSw44yWXcmLqsjNCcTr6nXhS3Y2rCy1IMHOJrYFszRpZ81G23LLPqypW4j7V8arkB5g/qIo4T3zMC+LAP8ZSPFHbbffvvtue+++15uxYoV17xEYPfdd7/YFltscViecWAENeSwSbWWUwWUWz/RXw7uquNK7wEojVz7ypvU+sPgVDXtoQ6pjfpFn9/p/I0wD2gVDLmtvjR4Uql+5WYArqdmsTPAUNlTlW6jiV1NdpDpxMTz1wZXBN+RBnvPpJ5r/Wr/dZXBnp3nT+yqBHFc8OSuujkarYlxIpP035fPcwoz3zg2KD1OL3cKntLVABXSd//Y4CHuT6pY6lC91d+YGuSs51JtPjapXm7PD548qae5PKZUxx/+Asp0cqnBMexP5+L7n0NmFqF8dwq+q9RtpDaa2LXFR3xVqT7iHGHs4uI2qt17UEbQyuZqsObDflLE8stnUjgmZX9VGPqaYeZbZva+3W677Xbj7bff/lZ59v1h7vvkGa6xyv3YIMlOnzq+a3U4ja0eLcvankv6OyBIteFchLGn++iQzRHmC1OVZpQ/vVRxSHQOzEqcIrL2MNsJZyrdO57/cle3WRJNLbldYm5NWCgz9o8areH5vfsPU/Xhf7q6H9v2RTT9a8EYfKaDDOGGOIQMGzOInMPe6bd2Na7cUKYzMHjrnKQNIYiIlMpEvPxH7qsPDPb3/H/7UiPJ9CJzqdso/1bqWWI9zOTtbMFMHT251H37RGeba6RNTTBj/zX4obImTNP3S5MmhnxM1bt+8W3PRQy/Vn7/T/CzO+2000223HLLm3s2s/otwuh9vaUOnhcG50lnA5OzyW7Y0qB797P2kIb8tnokWfy6vX9I17wB8z9dvn9uIdr/wg6USzuNNJqNJzqr0Z/BbD5ArLe5nxfcFqWe2mm5TRTXG8Cubrm0dZAXFHHt0FID7fPTtjvrZqWen6UTyofrgsHQsRpcqdTyWdKji6JtHvEbLT8rS82HGbhXvodvNIOQzulophsEb5rfnGSI5zfNTLZzfjuP7ab5TYrhQCTgv33Q6lRZF7R8M2DzD5fjYeuvPQbDzjebPvZstPY4uNFnYPCuAo++m0bv3jni+aHbbLPNzQ466KADg9ddtWrVTa5xjWtce//997/q1ltvfYswvXO/rY7cPGV2HhmVZ4iEc4Y6bLYgqps6vPlcXdWgFt1y0mL1j8y9MGCkthvp1aV2ADMb+urTD20AdCBbMB14sDJXYp3Ya3QnLpT8oDU20f8VpTLSMfn9qlI3SNw3iLZN8IhGi8TZw9ApzgnMfEO8NOVjUMTQaOlJHy0/9k2jH1/W3zmX5WrWso5vuZGl+hURX3m9UTFelv90dGL5K7vqXy1NdXXn/mNT3z0nMPMNAwhf8cNKjYVGXQC4RfAF22KBwRiuBb7VkJHticFXRETfP2V9UMrzor322uuwHXfc8dZh+nvsscceN95hhx04vrx4o402OjLPsG1YTXlAUNtqyyeW2sdWw2wdBl+Xb+w3qdZ+Kyw2+owMvkBgdO8t3KVGOTmp0UTm+QAxm1j2ja6dO5Xr77u6rXIQ0c1YP2+0WUMoJDRRmF6IvlepouQgMi8YzDBAn16pwQANbOjnlTWHBPj/po0WEXQ1g+twc2uWdbYZzndLZ2dL+F90xFaMzLf/j5NqRSeiE8uPKVVs9g5L/oLBDCM8o9Q06PmkkG+VWk5BIdzXvsDAbLA9AyhrV20z+sXfwoDXye++X2yxxRa3DTNzDHp3GP8+ocWSt2rynOCwH/w9pe4HZ6mX/voYfGubkbyTeqV39ysRufJpny3XCGcTNOQdSo16SoxkKDHLEV/nA3uUGsLHZg2np1h/dSYYjyVGLPuStyv1LCqzh22TZhm6mxnG3nK0AQHzYzQi/GKBjoe5zTDEdWW1d5mYjjbLXqzU+jiyzDA4bB10k3TuO2T2vsfOO++8avvtt79hOutt995770tGZL1e/rt7GIDDB6Pb3bvqK47J2Dakt1jAsIbRMDD1SXuuaP8Rl5UbbNpwndBVEJDyHikrZ5YbBu+57bbbXiyi+jVD3yn6+EH57Tjle6Ssh+Y5thbr4uwQVAXSoL6lj62GKQbfNHiH4H1ST8RyBjubZHoHnJHBFwcYSohVB5YaPVVDnSEKxzTo/I0BbDx4YvBhk7pJ4JiunlXGamsQeHzoVaV2cuKvDk+EZezCCDojWtifa5RqdNNBepDGAoP0lNUVs6MZxs4ArYzCTdsBx4nDzquHp6M+OmLrxdPBbxZmv+VlLnOZq2y33XY3z3+PJtrmWbPfY1MXGI+EY+0dAwC6MkZcLGBMu1/Qcp+B6ohSRff1gWf8Tz8XmEHYaNtc9wgeFXxCpJYDUi4OPMdmQDs0ZWaAe2zq4UYpszZ8QqkHBa4XSEENbXLhUvukSd1cYoCghy9obIAR1oZPlypmmQWM/Eb9DRrcGnND6+n/JLqGps/34ldXAwX8sv0+JNhbS0sVhYmznjFzi4TiPpHyYY0mMvewCAxONJeGcENmb/SJ0w+AxtxQbDED1qPSMUkuvRU9M9nBZnPP7rrrrofnv170T6cnydCHWdQNdqzo0qCbrix1SfJI7y0SULOkR/XRHoJQPn2tJ9YGMzrRHpMyuH6/1LbB8KcE/x5mPCK/2RGI1Ta2GLDsY2Bv0Wf+L3hy+946Yaq/sKL/sdRvHTKpfuecjri3jgy+UDDDONZRzaKYVegcxrIzGGOmYWiMSY3k8l9tBucscmxX15V9Q7RSszbGILI+IWjTyuoZvFQbAI8vgwox1v3FNLhZysGM0jXDmsGPmn4AeKehQBmCE14xzLttyvfglPWYPfbY42KZyW5qBj/ggAOunBncppZj2wzOEm9dmJMHf//HlaqWbFJqmvutndqCAumAqmGFQ3q3nfl/FnjzkDIYBzkkDTM4/4bbo1MeOwnF0kPbiKN8nuGVqA0NDmcqdbnf8vSwrnr5SUNQTisvfRSXkcEXCGYagdGMwYtoTkx+YKlLR+uFKQang98reGRXl8Po33fpmg5e6oxCXGdco3djdsx8z64yuyU6aRP1zDho//ewvs5yVmDmGwYRkoMrZkAT19cHW5Wa77vO1dNaGZzusssuu+wepr7B5ptvfuvo4PtFB7+W+xtvvLG95bzK1AEvMsxMBcJEpCPfMsj0sBDlmwE2FQ42Qlb5ONVAva4PGFzvmketJFhmE7mHu6rQTIyH7Cv82A9r7cxjkaca+pDWhtpMmfrCrKNM1D5qA9QXqA0cjaQxPZiu9dII5wBmKvMTpVrF6aIvK1XEM7P1sK6KHxi8a1b0ICs6y/kgojPs9Fb0Uk8LmRbRB6u2zj5Y0aXHEMYKy6+7h3WlfVZh5huDSjBtRf/Q8Ofw7NQ7GMYzRFWMokx/CmMbkPqVgeXLlzu8UJn+mGeI6MR/77Cgi0KKPr6scUJR3z0sQvkw6m9KDTp5SKnpfWDq/1ng3tq3Wakeit9Gd2tEdPQRpS71oR/VVYnE8y8u1bai76wW0ddRJuvwQxrsPL9qtMF9ZO7FgJkKpYNZxjmkVHHVerDllvXC0Chdjcn2xuALgjYoiJ4KzdTWYF/e1cB6x5S6NozxMRadlLRAXEezspu50XTjHhai4We+obPyKiNd6MTKSmLpYXh26p0dSu3Iz8+AtjL3nxx8ie2T+f2o4AkR03nwUUFOyJVIru5eXur3+RagzaoGCAMo42MPC1G+GZAeGwA1x4qFtmDZXh9QyQx6L+vqmWyPy1U7WVXwnjVuYrh1fPThXV0R0U7qUhnfWOpg1sM6yjTUIZQn/cL7JMYRzgXYvNTzyihAZmGzLKYkvtkcQjdb6wXg3gzqLIIXQsYU1mTea0S/Q0r1fiOWmfWJ7KuC/NTRNhpofDRbwGKBpTkdlHg+eJnRi3sYyjlVJlb0fws64G9ZGJpjxw2W1NBTopagRX2x2eLwXIVPsqHF8+wQRFK09WcrE9QB5V8rvQWElaWmZ7UCiJ67oRURVnTLhJYStfNhpapsZnY2BO1nmdPgpG1ET8X8+ogZfz6wrNTvQ3kxkHt/8LwbYZFBZ9ew9E0zGpGLN5Q1VbuDuJSuszMO9xti2F4U6+pM/ZP2++CuRc8slcmJ4OjVvuilLqGZ2dBvKosH/11qGlQDuiP6/cOf1A4wVaZevJzUKKvq6Rel+p8r3+CLjumHlYFblaZ2hCaemwWlYSY1mLA4f1gaCwUz7cLAJr3XlXrE1KPLhg1t2py/uvyKT/6tRl+za44uuXqfFOK7jyi1b6CJ7fOBWRH9Z41ePbCOsLhAnNa4y0vtIHSvo/zu6k6w3hiyPvBfQ9sFBRR4T+h9g68JnthVxsBYJ3X17O1jStXZWLLpcOijSh3Vdaqj65cXBgambXD/Uk8voatKD/3k4c+hLIONISjqi91xdpEZwHRq+6vN/ganD7XyWQ5TVh5unH1OLHUwM2OjOfwQdenDVgoWDGbKx0j20VLrmGhMVLf2vj7YtNTTaj6YPOsHxGjlsD5NHbHzziD/kEb7vrYySK0W/dXZBoAU866GZv83lGqHUG8jnJvQ1QD2ZlziJdRhN+hlNMUMwgWJxUV0Jc7aL321Sd0iyKvLt4h+mB/tkICVQe/Yq22ppE/Pc6Va8i85m945BD1x6I3TdL1RmdtOqF0n1XFHuCHlEKGVr7YBj5Vc0AT2B4H90b0YG7TX3q4z20kNmPbOo+13591FBVpZqiun2YwV+twGDSltebFMJt+Wq5RbO1kWXBakNtnPb3lQm1h6M9hrs4PLhk8KtcxqpYK6hdaWjGrUOHCGuh/h3IHBWvrw4LHoNK4oLj0Tz60jwsZwf66GH/7bknrmlVhcvdNErgYMI7bv2nlmBEdbPuL4gT62q0s1f8n1JV01VBHjzBqrZ9XFhCGNrg5sjw59j6CgBpwy/jypAQl+VGp+zT6fabSObxaUX3oqIxL6/rnPYIh+dqkSC/rtpc7mROMvlwaLXb4p2LXUfPyuVPGZ2qEcBhv5QR+W/FBderUj9PHoXB1X1PeLXF86fHBoI9hgOL/uh6W6yv6pVPVkgw5UIywCtE49/HxK8LuT6mF093Tsb89VV9Rpd8Opt9dyQ9wtzP0VmHc5fAifK16X2Y+4bnOKjmPn2Te7uqWS3m0PtvRYmtGPznuss5ahjh86zmIzgO83NNveNVfbIFcmL0JCf3pSY8m/P3hqV2dvu6DsfSbOPqWrMciIs9QANL9u2yKV1YkgZrGvljoAEFVPLVP2hoUon3oacPr3DJCO1O1H89+OuZ48qfHaGAeFXvr6pEotL07exZa7fvBxadfTc8/hEQat73RTIamGuptKnzHTxh2edKQw6Rk8VrY8jHBeQBpm4zTU8kkNVk//FNReWB6HAdj7u85g/n6nA4jBs1lw07kas8y5ZKJ2iOLKWQIjEN2J40472WrjjTfecaONNrrk8uXLd7jIRS6y3bJlyy651VZb7bbNNtts6f6kelStq5MuOAydNKgO+pBO7be8WxaTD7O75UA0x56VjSbSOrMLLWCE54bOTqzt46CVelopUR3sFuw9uIb0z2Xoz07r6tZeIjiafzi3Ufu2SWS82LS9gy1si+Xso29YXSDSTzO4OmCQVQ8s5yzvpALqCNHcas25XsgRGgyNNTRYGvTxAijm+qw0rAio/wi+DzO3zr7W++5P4f+3dy/AmhTVHcB77iLC3c2yBhZQF3YvPsDoSpnSKJa6gEpekkppLOOrQFETX4SKEo1GBY2SxKI0vlIqiQjxDRrK4Ks0IBQqiRqVSolGixXEoAZ8RTSJ0ZzfnOm9w8e9y33iZe1/1ak533zz6Jnp033O6dOnpR7eOai3bOyPB/0kyDpe77A/zj8xiJf363HMs4OeMBz/grivoRs937vdq153NTFUUiSPu3LITirvnAT+355KIVcmCQfNB5e0n9rKJjcn/n+7HB4z0eR/YitC7GneYZfzrI9xbsnx5Ls7t6Sav+v+txKMjlDPv9Glr+Ga4Tmkv7Zc00/W5aIJ/2h/vIvHxDO8GR/04r2GhJbdqL4MJKOrZ6UZcKZ61p0lI9ka1gJ8KB+PMImtDrox6Mx1uQaVVS4u8H/9wJOoghi0Mc67MsiiCJIy8qx/L7aivtjY3w8y3ZT3WTrlp8SxvM7u+9Tp6enj4r8vBJ07uubE3VYW9dm7XAXVSiHW1dKTXxO0cyp79M/F/zdM5frnH44yeQ5ONIE+eMNLJmTgxXI/MY4R5XdakHhvUWYCUeCGMjtXe9UF3POhkt51vgSNlSmbX4oyet4jpjI/mhVTHhT7zvf9gh4VAv4GE4pC43rhMO1zV3nrdYN+e3huZoyZep6PWt4EfC2hCtNUJqCnYpsSKpE9/rCpXDpXgr4+d9kkfPy9Mrc4L7ShM55oqjaVm4qu17BowH7r168/ONTzIw899NCZ7du3HzEzM3PUEUcc8Stbt269Y6jr26NibVORamVaTYwqqmejskov5bdtjZvmIa/LJvOQGwnAa330jPUadfleNM4LzrNeoeJLbdX/qNvVwqg8yPfYNvAaLg2W94yntTBTmGZMqf3iW2mwLS65Ye9cd26u69bREtemklPPxeILpmlYCxgEu5K44/+MrVlgHE7XB1GZ2Vo8x3oumLzMTXrc4RgZP77dZYyzWG35sh8XJHCCI+dpUYF+z71i+8yNGzceF/u/FP9b82vVBdwzjIiH/Lqgy7q0tzkHOaEI+eVBVs0URMIh9a0uI7skjRTYY2iIQ83z9U62Yb+gF0NGris01Pl60Y/0BSi3qoB7jmu7fCZCfkXQdV0OX1ox1XeidcmK6/tbk65P2ECwoxef77rm9DvXeHd91s+V1oOvHVRhGj6YgAy22RuCFzDBvvpIlwEwx3Y55xsmL9NjdB3SWeecC/wQCy5a7qQukwW4x3Pj3ifG7w/FlmlQh5QEzqx65Z+A8WH35gnnLf9B0A1BhpcsHOA9zARdNhynQfj7gdcwvWzgTcN98sB7TsKPF2WmZ8OL8LtVMHwLtDXesXf+nS57W4LomcxA6yfQdOkv+NDAP9k3UDcI+W7g23qmT5aMUjNxyFApr33DWsGoIvRrXnXZ4lNNOZNUeKq537vUy/lQr1VyvFV8NO/qtiAedTHcB4Yw3ztU9QP3339/XvQjN23atCV68A2xnzd3ZjifOrC6Rvgs3NBEiOoJt+293V0G6rBhQRTYOJ56HPM9rtRj3vH1OcSJ12vdGujq9wiS8JB/Ac/z7V0L5GGa+Da85Lzhpryap7C7Rtbz+JP5cZ+SATBUdN9bDP6t9d0aFopRRagfVuCJcVuqqGgzvZt84irFYlXo1wRRCznVBLd8Pnjjq0JHBbY8PwR/R8neTYir4Rb3Fu99U/1w5fDQoC+WXBhA7/rZkmPUbGxluqjLJBj2/XNJoWVq8IIb7jIjT3mN/+q1nS/JgzBRvKAe6ZQcI5bbOc51v60lx4kvKhn5VlYByuX5Lozrs60/E3TZVPpDqOW+gXFwQUhXBIlsm6wDPSZ++2YcaQKWjPN7Pqp9E+q1jokPae52rzJ3mQqY6mahOUvQ7Eqqt0Cwx12rRrKJcDqtyyyr9p8VvJlH9guUqKrtzrKCAj6utCVnULnHp0qq6D+N/6+JLSefZ/3vMut9dhx1XRw7Xk/1roE3BfYVA+/5TnKtkvHuRw/7LxjO+UnJVFZ6PPu/U3JG12TZVgK82p5jZ5ez9njE/bayqiEzvJ77y/iS00An68Bc6E23klNGzRL7cclnWnBlaFgb0OP46Bxr1HMzqCz1WofUdjlhFgC9CUG4Y0l1XYwyj72ZTbKFCMBwITzHD6F2jJDH1YJ7E4IaX03V7MMqu1zA4LBhvzJX3jnbBp6nWE9cwYlWMd7PVGGmAE2hBsfwZ4gJ7wVqkRrRQkBl9nxUbvcQQ9/fL+5lDjunKZODc8y3XZBjLL654/T23pX3cUzJOPfdtgoNaxRD5ZDN5KNRMV4XdM/4yJcFWQZWUvte2BcBs6w4cx5T0kHFA6vnE954YdDpXc6lxhs/Xq2eQaUUI0591juLLWdKqPTnlszIAq8u6SSjghrDp9JS3fXUeMJMw5FAwzU1ZvbTSNjceNNkqfh6vZfHO9Ngnh909lRGia3GmL9GyjRdJo+IMkk6qNLm+5t/wPzx3HPCdx+hzgNn1jCfzgl6zuzfDbdJDMKN2M68sFT0XvXbd999/d6kYk5UhluC6YlUPIJVVXR2KfsVf0lJjyx1lsq8Wio6DzA1nH2tcXFv3mVqOR4R5OtLqtzGvNmbeA0QgXbM0WVWRX9KyamV+DNLPgez4/1Do2X/Z6enp2diewOVOUiYb79s8wrDdzJywasN7q3sBP/G4XdNEnFL0AA7/k9K5mJj2y90bnjDWsVIwKmVhrhk1dw/KuTjgx4ZfB9vvUgBpxI+oaS6SmiMpVaw6fSCajtnjt+rBR5wQ3Psb6i9EzAN6n7lrD0Ys6Xy+5WbJjek4lfYX9Uaz1MDZR4YDeM9Q6jXhZAfFZrP/aI372hAGzZULX7FoHGiRTB1wHtG+5RMRMGWdsxCQEvxfrbFMzBZCLkGpOG2jpGQV/KRzwrZNrPIpAVq9FllaWOfR5dUITm8VKI3lFRn8ZIknl5ysoae44/LrC27EjAsJoHDH5Yc7nllySygQBX/04HnbHzpwGuYXj7wvOV4Kr2e8GUlEx1sHngCz7xwzG/ts88+JmqcEu/t+EMOOeTgEOrfDOF+2KZNm9YLJNm4caGytmBsK/k+PYsG81UltQoCPicmGmqmFM2KYB9b8nzfqWFPw4SA64Gpaz8MXiWm9vk9djItFKLanCtopHq1P1FyeSM89VIPenlJ7/pSGpH50I8MlBy6Iuye45slG5GqzqrxNwy/NQKGtqi9HGfvKWlGGCoi0I6R6kgGFPv/sqQmYP/b9t57bwEm7w0BP3XLli3UdXb3joMOOugAfgwq+oSALRd1JMIogHdYvxOtaSGoJojG9UUls8saJmzYEzEScCrai0tmQiVwTx9oKW5gSQKkTqY66rX/qMsEEXptvekJJVVhPgA284rZ4yV7WpFnvPvwuBGvPHpoIMBUeaC2116MZ12ZQC9ejwfOw2pUU5MFmfBeHx8q+v03b958h+jBH0hFp65T0asvYwWFvL5DjY53+LySjjFlXQiYRxJYmI8gaeRzulwyevK4hj0YejuVhrBT/U4uqVbzHi8W4rv16NRg3l2qJfVZg4LXEKyEiv7ckgJIwJWbcw84yB4x8AS6+gaOKenxB3Yn2xX0+hoFMFxUGwHQCNgHBMxQI+G1iMKxoa7fLgTaVMvjqO4hQGbZ8XH06vMyhIhG9VclnWL8Bd4bk2deTNzL+3a+kQDP8+IuM63yIUjrNLPCjVDDWsPExyWIVZ2Fbw2/tfiLxYklz/1USbvPNanonFT2I8K+KOg5Rzix5HXeVdIMMH+Z+q83tv/a4bjvDb/hGyWDOQgfL7r9MyWz0QoYoYbzD3yuzDoFHcNu52xzrghA59j/+RBynvofhHALNqEFyYTymW4Yi16MAM3zfJ8t2TAxFb5d5rG75xDW+nw0J15y5sjphDu0jL/pMqXy5DkNezCofpwvotPYqKeWjODSeywW1HUOO8LCRsUTEtd6VZcTYZYb9WU4jL2sd9KDu/7jh//M/tLbQt3Co8rswgzUbaou6M3rWPDWksNj1XPOZCHQwIZ96FBmPare2jNYc/vpA/+0kmu59Scs4/k0rIYhfQeBKG8qOYR3s0CF+h4n7uU5OR6ZIoRchKGEFvwHZv6JfJs8p+EXCFRTKrYeimBQD4VJLhldzq2mvloiqI/bXkYFI3SGiEyo0auxoetQF0/x9oHXwFQBJbw1ok5PX4edNBCVV6A6rAaG0mohjy6zDZ7IQPOnPQN7VgARvtd46nNN9MorDtcf7muYU4NjvW9lpIHwS/ROQKjvu9Jql61h7YIDjBda8IiKT02k7vEqLxldZv4UVEOdrUkYJg+bFxMVUuX9bkn1k/NM+ajohBhPHQf8DwfePr/Z/zzuTAe9Iy+6/Q8qmZPc8Rq3x5ZU/f+iZI/qGBFzAkyovIJEmBr2f7fLVMt94AleeXnVFxHnf4uo74wjr9Lg9JPoon++Lld7vRRf0ha/2fmLee8Nex72LjkefkHJWVQi1Qy1LDggovYsMKpUxto/EHRel1M3l1PRDB29taQ6bVjvnJJON57lvytpb7p+bxIM/EtKjskDFdYzelYONuqwnnxHySEkw1EEmWrMqy6Y5i0lZ5uJDT+7pCORynx2l5lZ3cOMvbNC8G7Ho14zqEwmWlgIxu+n8oMw7xLu6rWfSrw+6B/iOOaLMfsPlWHiyRirEErbcBsHYeKh5lHnKKMOU3sFhdivEdgtagWdpGXA3GzeYM5BoErX8XvJD2R/VfGlMer5LueC67GBwPKeg2EwAl0x5o0IVImggdTxe+bAzPAc8sBJeuh+spc+gOAFHR7Cfd+g6clUSYvF6J0Jsjkq7vFrQesF2cT2d4P2i3sfHfQ7QWLUl/t+G35BoJbwyFJV2eXvK6n6cSYZhvlO0J+rTCr47tTRUSVdicp3Ysly6MVpFuLELynDWlpRlquDNgX9LATLbyu3XDecI0BE3DqeAPPEU9cNuXFk2W946qSSqjhn4zEDT6vRsDnGvPit+BAw2Vu38Kb7He/h7sG/a/369VfE/e9FuHf3bm4Jo/cmwu7SdZkh9/ANGzb09zMOH7+/io9jJMdciXfcsKdiVDkw55acP62S81xfVtJ7zb77WMm10Ppzhp6ynnszVNVyBUCVNmvslC4nf1xcMnzTOO/Hg86NsuwbFf/DQRfGPTmhpEeW/IGKbcTg4yVDUnnTL+4y4b+xcfuptob4XJdaTkuxn5rPicXGfWOXyxpdEtd/X9DmEOKPhDBfGttDYvvCUM3fGPzMcm3x+n67HIpjdpwR99vifkGfDl4q6PPiWa+I5z5q0CYmL9PQMCfYmYjkqujmEG+OCtWv6RXbg6KnkgKKjd2ngkKrAKo0gawgODUIhYe/TgS5S5TpThqSqPCHxDF3HmzVzVHpa7ZVHv2ZgTdkx94mFFTgnh+Ar2PPVPqqonsPdT64lWAOH9Tye8f97nO7BP6ooP2WIuCj9+j+Gh9RdMp3v9jeN+5JK6GqS5NsgYq9ggTeTDXhblgqONt4ik8o6Xyr6qx48NOi8j1c5VLZOZZQqKo9LQUTFfWUkvcDfgE96tjDbeVQKjeV+StRhjtHw/OzuLf84AfGvq/F/9R1DREthDrLln77wNNKziiprhsWpKG4Ls3gN0oGm5xf0t7H/0ucIx+5+32DkEev/X3Tb0OYjwy+V5nj3g9ZiIBrkMbk2QcBF9WmHJ+O3/wD1wddG//jr/XfVK5osssJ19CwVAha+feSIZ8CPwwVGdeWqfVFDlA5VeaxcKOo+KPLLAwTlZWq/E8lc8wJpmE7KwPHnwgySRDuEXRVCNXFBDzu+6Wgf4vyUJ8vjbJdFaQXf09c+6rY3jvIemtXTeVa4a5n/wldpk7eGb8FsjBPvlIybbJG4aogyxLLWnN1nEtNPjzucWUI9s649/bgPxHb/1g363Qr88H7qsdUqkIexDHoud8W+4zfG+KTAprW8umgq4P6deCbcDcsF8aQRb31avtUOq+6DRs2bJzKZPsbh97qgBDwO4Rw7RW0NWhLCHi3FCGfD50aPQvjwN0gEIdEOQ5UjhC2rVGOQzU28dvUTmmHHXPAVC4EgeeBtoYZO11GUsKyb5ceawLMWedeHHiG0oAdX9f3Wh/XnB4EU2acXwqyeIQloCTSsNbbbEkn4L9BoJ1roQrneRYLFljEwP/MESMC9v/yVC4LrLzmpjI36vppDQ0rh9prBJ0YvOWCXhKV8TiqcQjzB0OwHjw9Pf3ToCtDyPcZBG2XKokGIanq6JIxXIP6fUGU4fVxn21RhhuCriPkcfsrY3tj7L9rCNHHgn4Q939AbM+JfZZpMvuN2SFo5VlBT4/r/FfsPzNIdNuHS86+u8k963NUQR0auDl74/GzVhqde4r3Fr//OniLFwjA6RuskuP0r43y8xfIzbZ9KlMlL/u9NTTMi1HlfeaQ+um1se8R0XP+OCrjB0OoHmwoJ/77YvzeR8Wvlb3SZIWfD5PHTQrJQGKtec3fFPcyjPTDuPf1ce+7RVm+Yo2uKJ+VUy2o+KPY7ojf7xjKbgUYQ383xjVMrzw5zrH/NVM5zdKIwUsny+O+VbgnBXzy+ebC6Bonxzk/Cv71se3fW/CnB22Lw/42jjszrmuJoSrg/fJT8123oWHZqJV7KpctpqKbH03F5GS6a9D6oF8NYpOaL02VR2ArMb+eSM+En3PNtIVAWaYyFfQBcZ9Ncb8uBPvgENKDBj9An2plaJQsq2uJZeozNVrZmRtGAbZ1uVQydX1bbKnAyiXPevXaLwhVALtU+V2PaWPrPlT82w/3YmJI4MhZdtBeuT7cYVEma8T1Jk/wUmxJC8VT3id4bGhYdRDwCZLvjTPr3KiYDwjhtsTtR6NiWrTw6tj/hfifM0ygyNe6XIHF+PTXu5yrvOReSaVXBj2o4JLq6GP742vAySDku7ZoaCAmNYKb7J+rXHX/XDT8D0JH318ynbT0S9eUDNaRTuqa2Pe8Lp16X+1yjNscgH8N/o2T5ajP2NBwq2FCEJ5Ycqjq4iDBItRiywwbv+3X1Qo6Moit67c837zghqqOnbj0olEFFxFmgl0FnYCPhXpMk0JEQOt2TItFl+B9Fz0nh/m7Sz7rs6fSJMCLlzc0Zzjs7JIJK6SYslhFXw7lbmj4uWEkHAJKOInutS7VXymNBMRswAfdHx//3z/oQVO54ikhV/l56OmeuijSNBe/1nRT5amSX8tZ99f/DGmZXkrdp708ZCqXHpoJevCwZQIIIuJHYAqY2urYJTUsDVWRoScAAAP6SURBVA0rjtrbjGnUS1qw8ElBjw26Uwj9R2P7yTjGsNQ7u1zeV1CHGV/i383iOn3gzfh6xsCbxbXrfnVb+Xrfyrs31F568ph67lKo5Piz6bRmbgmZvahkskmRbu8tOdXWyieeyfNJGPHSoMvj/o+JMh0fWsY5sX208oyu29CwNjGPcCOrkZ4R9GfBc8D1EyXiOCufCtbwm4pOQPCE+p0DLwBFJpiqwv7cMQij4TNlYm4IgDGH3m/BN+xs/MPKMLkljjHHnOAzYU6Nd/GM6enpr4WQn1oboDkakYaGtY1BwG8fdI+gu69Ldf34oEdNpZdab22BBIEkZodJqSTunADhDys58wtvvvPPFSMB5A2XCumYIKbGw4N+3f6SGWDY0qa27oh9jw4SRcfRZkjOexDA8tAQ9LvxF3hPDQ23SdSefD7P9Qg8zjXBxLMG3rxzGWXwJ5SMff9AyZlgxqgvLDl91VRO+wWGmCgidlzACBv47OE4DcWrh+PYuUJs8XpaOdjwQnE1OI5Xhh0Dz2SQz0ziitd1ObHlvKA3x3PcKejsoLfH/pk4Tgis8vbzyz1jpardjJ2BqKHhNo0q2Lcg4FeUVG2lTSKc+OeXnJeNl3FFRhY8YSSIeOf1XvuSyf3rJBRkkYfvD/zRQV8eeBqBABa8Oe5vGXgRbacN/Lklh7RMQrm0ZL4z6vbOKLuY+Lq+25HD8f7TS39z+K1MNxPwsQnTPOQNezQmBNxkEon+9bqSMMjOarqkHh1vjFh2FceY5bUtSGipRRUOLjkppar75ndrKPB6Zf85Rlaap5bMAqPnNiNOj69RIOicfe6BJ8TK8uySmoPQUQs7/H4I6YFBzwxyH88hqeQfBLmf4cKTy9Jyyzc0NAzYu6S9zu7dVjL18wtKCqLppK8YeHHkEj1QnzUUeOr6SfgQSur6I2NrP6HX87oWxxjjWHooqZ56TPbG4x56osFqaGhYBgidnp7zbUdJtVgqqV59HoiKLltqVdH7udolVfSLBl4v/taB5wdgb/9fmV1HnNDr2XuMhXkuAW+C3tAwByZt8vn4CeiVqcVbS9rqbHbqN0F9Wcke3Pj5K4dj2fB6ag6wJw28HlzeObze+5iSyR6o9tKi3qWket9DOeu2er9rbz7+v6GhoaGhoaGhoeEXHGMVf2wHL4SHsWq9G5OgoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaFhDeL/AbL/6dpoj+OHAAAAAElFTkSuQmCC",width:"248",height:"248",style:{mixBlendMode:"multiply"}}),React.createElement("rect",{x:"184.055",y:"54.995",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"170.059",y:"44.06",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"200.238",y:"77.302",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"212.048",y:"87.8",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"206.799",y:"83.425",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"204.175",y:"85.612",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"219.046",y:"103.108",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"154.751",y:"30.064",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"188.866",y:"63.742",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"148.189",y:"34",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"134.051",y:"31.707",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"126.124",y:"24.771",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"115.385",y:"29.19",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"95.702",y:"31.376",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"91.766",y:"27.002",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"90.454",y:"32.688",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"184.389",y:"45.58",width:"2.187",height:"2.187"}),React.createElement("rect",{x:"162.185",y:"41.873",width:"2.187",height:"2.187"})))}var Mt="ai",Me="ai/ai",dn="https://wordpress.org/plugins/ai/",De=Object.values(ze()),un=De.some(e=>e.type==="ai_provider"),Dt=[];for(let e of De)e.type==="ai_provider"&&e.authentication.method==="api_key"&&Dt.push(e.authentication.settingName);function Bt(){let[e,t]=(0,L.useState)(!1),[n,o]=(0,L.useState)(!1),r=(0,L.useRef)(null);(0,L.useEffect)(()=>{n&&r.current?.focus()},[n]);let a=(0,L.useRef)(De.some(G=>G.type==="ai_provider"&&G.authentication.method==="api_key"&&G.authentication.isConnected)).current,{pluginStatus:i,canInstallPlugins:c,canManagePlugins:u,hasConnectedProvider:d}=(0,ue.useSelect)(G=>{let v=G(Oe.store),z=!!v.canUser("create",{kind:"root",name:"plugin"}),b=v.getEntityRecord("root","site"),j=a||Dt.some(T=>!!b?.[T]),q=v.getEntityRecord("root","plugin",Me);return v.hasFinishedResolution("getEntityRecord",["root","plugin",Me])?q?{pluginStatus:q.status==="active"?"active":"inactive",canInstallPlugins:z,canManagePlugins:!0,hasConnectedProvider:j}:{pluginStatus:"not-installed",canInstallPlugins:z,canManagePlugins:z,hasConnectedProvider:j}:{pluginStatus:"checking",canInstallPlugins:z,canManagePlugins:void 0,hasConnectedProvider:j}},[]),{saveEntityRecord:f}=(0,ue.useDispatch)(Oe.store),g=async()=>{t(!0);try{await f("root","plugin",{slug:Mt,status:"active"},{throwOnError:!0}),o(!0),de((0,m.__)("AI plugin installed and activated successfully."))}catch{de((0,m.__)("Failed to install the AI plugin."),"assertive")}finally{t(!1)}},D=async()=>{t(!0);try{await f("root","plugin",{plugin:Me,status:"active"},{throwOnError:!0}),o(!0),de((0,m.__)("AI plugin activated successfully."))}catch{de((0,m.__)("Failed to activate the AI plugin."),"assertive")}finally{t(!1)}};if(!un||i==="checking"||i==="active"&&a&&!n||i==="not-installed"&&c===!1||i==="inactive"&&u===!1)return null;let p=i==="active"&&!d,B=i==="active"&&d&&(!a||n),h=i==="not-installed"||i==="inactive",x=()=>B?(0,m.__)("The AI plugin is ready to use. You can use it to generate featured images, alt text, titles, excerpts and more. Learn more"):p?(0,m.__)("The AI plugin is installed. Connect a provider below to generate featured images, alt text, titles, excerpts, and more. Learn more"):(0,m.__)("The AI plugin can use your connectors to generate featured images, alt text, titles, excerpts and more. Learn more"),P=()=>i==="not-installed"?{label:e?(0,m.__)("Installing\u2026"):(0,m.__)("Install the AI plugin"),disabled:e,onClick:e?void 0:g}:{label:e?(0,m.__)("Activating\u2026"):(0,m.__)("Activate the AI plugin"),disabled:e,onClick:e?void 0:D};return React.createElement("div",{className:"ai-plugin-callout"},React.createElement("div",{className:"ai-plugin-callout__content"},React.createElement("p",null,(0,L.createInterpolateElement)(x(),{strong:React.createElement("strong",null),a:React.createElement(ee.ExternalLink,{href:dn})})),h?React.createElement(ee.Button,{variant:"primary",size:"compact",isBusy:e,disabled:P().disabled,accessibleWhenDisabled:!0,onClick:P().onClick},P().label):React.createElement(ee.Button,{ref:r,variant:"secondary",size:"compact",href:(0,Ot.addQueryArgs)("options-general.php",{page:Mt})},(0,m.__)("Control features in the AI plugin"))),React.createElement(zt,null))}var jt=s(st()),{lock:Zr,unlock:Be}=(0,jt.__dangerousOptInToUnstableAPIsOnlyForCoreModules)("I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.","@wordpress/routes");var{store:pn}=Be(fn);Gt();function gn(){let{connectors:e,canInstallPlugins:t}=(0,Ht.useSelect)(r=>({connectors:Be(r(pn)).getConnectors(),canInstallPlugins:r(qt.store).canUser("create",{kind:"root",name:"plugin"})}),[]),o=e.filter(r=>r.render).length===0;return React.createElement(ye,{title:(0,Z.__)("Connectors"),headingLevel:1,subTitle:(0,Z.__)("All of your API keys and credentials are stored here and shared across plugins. Configure once and use everywhere.")},React.createElement("div",{className:`connectors-page${o?" connectors-page--empty":""}`},o?React.createElement(y.__experimentalVStack,{alignment:"center",spacing:3,style:{maxWidth:480}},React.createElement(y.__experimentalVStack,{alignment:"center",spacing:2},React.createElement(y.__experimentalHeading,{level:2,size:15,weight:600},(0,Z.__)("No connectors yet")),React.createElement(y.__experimentalText,{size:12},(0,Z.__)("Connectors appear here when you install plugins that use external services. Each plugin registers the API keys it needs, and you manage them all in one place."))),React.createElement(y.Button,{variant:"secondary",href:"plugin-install.php"},(0,Z.__)("Learn more"))):React.createElement(y.__experimentalVStack,{spacing:3},React.createElement(Bt,null),e.map(r=>r.render?React.createElement(r.render,{key:r.slug,slug:r.slug,name:r.name,description:r.description,type:r.type,logo:r.logo,authentication:r.authentication,plugin:r.plugin}):null)),t&&React.createElement("p",null,(0,Rt.createInterpolateElement)((0,Z.__)("If the connector you need is not listed, search the plugin directory to see if a connector is available."),{a:React.createElement("a",{href:"plugin-install.php?s=connector&tab=search&type=tag"})}))))}function mn(){return React.createElement(gn,null)}var vn=mn;export{vn as stage}; From 49163c836ff5edda93854f1cb8439a4a2d63156a Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 7 Apr 2026 05:31:37 +0000 Subject: [PATCH 15/57] Administration: Improve dashboard widgets border styles. This changeset fixes a CSS glitch on dashboard widgets bottom border when they are collapsed. Follow-up to [61646]. Reviewed by westonruter. Merges [62206] to the 7.0 branch. Props pratik-jain, audrasjb, ankitkumarshah. Fixes #65017. See #64549. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62214 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 28b881d363c7e..b317af45e023e 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -2281,7 +2281,7 @@ html.wp-toolbar { line-height: 1; } -.postbox.closed { +.postbox.closed .postbox-header { border-bottom: 0; } From d6a94110c9e8ebdcb3bea79162ec61ffde62aa56 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Tue, 7 Apr 2026 05:34:48 +0000 Subject: [PATCH 16/57] I18N: Provide gettext context to disambiguate translation strings for "Bulk Edit". The "Bulk Edit" translation string is used for both verbs and nouns, and may have different translations in some Locales. This changeset helps disambuguating these different contexts. Follow-up to [61255]. Reviewed by westonruter. Merges [62186] to the 7.0 branch. Props audrasjb, shailu25. Fixes #64994. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62215 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-posts-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index fc039a7573f19..c7d10fca217ef 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -437,7 +437,7 @@ protected function get_bulk_actions() { if ( $this->is_trash ) { $actions['untrash'] = __( 'Restore' ); } else { - $actions['edit'] = __( 'Bulk edit' ); + $actions['edit'] = _x( 'Bulk edit', 'verb' ); } } From f57dba337abbecd6711fd101e16e763716a38d86 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 8 Apr 2026 12:42:48 +0000 Subject: [PATCH 17/57] Block Hooks: Set ignored blocks meta in REST API response. Set `_wp_ignored_hooked_blocks` post meta in the REST API response sent from post-like endpoints that support Block Hooks (see `rest_block_hooks_post_types` filter). Previously, it was enough to set that post meta on write (i.e. save to DB). However, due to the way real-time collaboration syncs posts and reconciles them with content received from the server side, this information is now vital on the client side to ensure hooked blocks aren't duplicated. Developed in https://github.com/WordPress/wordpress-develop/pull/11410. Reviewed by jonsurrell. Merges [62219] to the 7.0 branch. Props bernhard-reiter, czarate, ingeniumed. Fixes #65008. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62220 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/blocks.php | 30 +++++++- ...applyBlockHooksToContentFromPostObject.php | 71 +++++++++++++++++-- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index 170d7c0fbf10a..cc1ac60667773 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -1196,6 +1196,7 @@ function apply_block_hooks_to_content( $content, $context = null, $callback = 'i * of the block that corresponds to the post type are handled correctly. * * @since 6.8.0 + * @since 7.0.0 Added the `$ignored_hooked_blocks_at_root` parameter. * @access private * * @param string $content Serialized content. @@ -1205,9 +1206,17 @@ function apply_block_hooks_to_content( $content, $context = null, $callback = 'i * @param callable $callback A function that will be called for each block to generate * the markup for a given list of blocks that are hooked to it. * Default: 'insert_hooked_blocks'. + * @param array|null $ignored_hooked_blocks_at_root A reference to an array that will be populated + * with the ignored hooked blocks at the root level. + * Default: `null`. * @return string The serialized markup. */ -function apply_block_hooks_to_content_from_post_object( $content, $post = null, $callback = 'insert_hooked_blocks' ) { +function apply_block_hooks_to_content_from_post_object( + $content, + $post = null, + $callback = 'insert_hooked_blocks', + &$ignored_hooked_blocks_at_root = null +) { // Default to the current post if no context is provided. if ( null === $post ) { $post = get_post(); @@ -1287,6 +1296,16 @@ function apply_block_hooks_to_content_from_post_object( $content, $post = null, $content = apply_block_hooks_to_content( $content, $post, $callback ); remove_filter( 'hooked_block_types', $suppress_blocks_from_insertion_before_and_after_wrapper_block, PHP_INT_MAX ); + if ( null !== $ignored_hooked_blocks_at_root ) { + // Check wrapper block's metadata for ignored hooked blocks at the root level, and populate the reference parameter if needed. + $wrapper_block_markup = extract_serialized_parent_block( $content ); + $wrapper_block = parse_blocks( $wrapper_block_markup )[0]; + + if ( ! empty( $wrapper_block['attrs']['metadata']['ignoredHookedBlocks'] ) ) { + $ignored_hooked_blocks_at_root = $wrapper_block['attrs']['metadata']['ignoredHookedBlocks']; + } + } + // Finally, we need to remove the temporary wrapper block. $content = remove_serialized_parent_block( $content ); @@ -1449,6 +1468,7 @@ function insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( &$parsed_a * * @since 6.6.0 * @since 6.8.0 Support non-`wp_navigation` post types. + * @since 7.0.0 Set `_wp_ignored_hooked_blocks` meta in the response for blocks hooked at the root level. * * @param WP_REST_Response $response The response object. * @param WP_Post $post Post object. @@ -1459,12 +1479,18 @@ function insert_hooked_blocks_into_rest_response( $response, $post ) { return $response; } + $ignored_hooked_blocks_at_root = array(); $response->data['content']['raw'] = apply_block_hooks_to_content_from_post_object( $response->data['content']['raw'], $post, - 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root ); + if ( ! empty( $ignored_hooked_blocks_at_root ) ) { + $response->data['meta']['_wp_ignored_hooked_blocks'] = wp_json_encode( $ignored_hooked_blocks_at_root ); + } + // If the rendered content was previously empty, we leave it like that. if ( empty( $response->data['content']['rendered'] ) ) { return $response; diff --git a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php index 5ff9f7323e0f3..4f95727524c8c 100644 --- a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php +++ b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php @@ -130,21 +130,59 @@ public function test_apply_block_hooks_to_content_from_post_object_inserts_hooke $this->assertSame( $expected, $actual ); } + /** + * @ticket 65008 + */ + public function test_apply_block_hooks_to_content_from_post_object_sets_ignored_hooked_blocks() { + $ignored_hooked_blocks_at_root = array(); + + $expected = '' . + '' . + '

Hello World!

' . + '' . + ''; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post->post_content, + self::$post, + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root + ); + $this->assertSame( $expected, $actual, "Markup wasn't updated correctly." ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "Hooked block added at 'first_child' position wasn't added to ignoredHookedBlocks metadata." + ); + } + /** * @ticket 62716 + * @ticket 65008 */ public function test_apply_block_hooks_to_content_from_post_object_respects_ignored_hooked_blocks_post_meta() { - $expected = self::$post_with_ignored_hooked_block->post_content . ''; + $ignored_hooked_blocks_at_root = array(); + + $expected = '' . + '

Hello World!

' . + '' . + ''; $actual = apply_block_hooks_to_content_from_post_object( self::$post_with_ignored_hooked_block->post_content, self::$post_with_ignored_hooked_block, - 'insert_hooked_blocks' + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root ); $this->assertSame( $expected, $actual ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "Pre-existing ignored hooked block at root level wasn't reflected in metadata." + ); } /** * @ticket 63287 + * @ticket 65008 */ public function test_apply_block_hooks_to_content_from_post_object_does_not_insert_hooked_block_before_container_block() { $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) { @@ -155,31 +193,50 @@ public function test_apply_block_hooks_to_content_from_post_object_does_not_inse return $hooked_block_types; }; + $ignored_hooked_blocks_at_root = array(); + $expected = '' . - self::$post->post_content . + '' . + '

Hello World!

' . + '' . ''; add_filter( 'hooked_block_types', $filter, 10, 3 ); $actual = apply_block_hooks_to_content_from_post_object( self::$post->post_content, self::$post, - 'insert_hooked_blocks' + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root ); remove_filter( 'hooked_block_types', $filter, 10 ); - $this->assertSame( $expected, $actual ); + $this->assertSame( $expected, $actual, "Hooked block added before 'core/post-content' block shouldn't be inserted." ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "ignoredHookedBlocks metadata wasn't set correctly." + ); } /** * @ticket 62716 + * @ticket 65008 */ public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block_if_content_contains_no_blocks() { + $ignored_hooked_blocks_at_root = array(); + $expected = '' . self::$post_with_non_block_content->post_content; $actual = apply_block_hooks_to_content_from_post_object( self::$post_with_non_block_content->post_content, self::$post_with_non_block_content, - 'insert_hooked_blocks' + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root + ); + $this->assertSame( $expected, $actual, "Markup wasn't updated correctly." ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "Hooked block added at 'first_child' position wasn't added to ignoredHookedBlocks metadata." ); - $this->assertSame( $expected, $actual ); } } From 068f80c1fc16f0a256b26721ee296b96fc05b6ca Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Wed, 15 Apr 2026 06:41:30 +0000 Subject: [PATCH 18/57] Administration: Fix focus outline being cut off for the metabox collapse and move buttons. Fixes an issue where the focus outline on metabox collapse buttons and move handles was being clipped. Reviewed by audrasjb, wildworks. Merges [62232] to the 7.0 branch. Props abcd95, audrasjb, brianhogg, darshitrajyaguru97, poena, wildworks. Fixes #65060. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62235 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index b317af45e023e..4c18ab586c359 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -3340,7 +3340,7 @@ img { .postbox .handle-order-higher:focus, .postbox .handle-order-lower:focus, .postbox .handlediv:focus { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); border-radius: 50%; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; From 8974b212a8edf0783ec357b92900f54ea036f306 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Wed, 15 Apr 2026 06:44:02 +0000 Subject: [PATCH 19/57] Upgrade/Install: Use new default admin color scheme for links on the setup screen. This changeset updates the link colors on the setup screen and the default wp_die() fallback styles to use the new default admin color scheme. Reviewed by audrasjb, wildworks. Merges [62230] to the 7.0 branch. Props audrasjb, darshitrajyaguru97, dhrumilk, hbhalodia, huzaifaalmesbah, ismail0071, mikinc860, pooja-n, shailu25, sumitsingh, vishitshah, wildworks Fixes #64962. See #64308. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62236 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/install.css | 6 +++--- src/wp-includes/functions.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wp-admin/css/install.css b/src/wp-admin/css/install.css index 71ea71c7d2863..9476749dd7cf2 100644 --- a/src/wp-admin/css/install.css +++ b/src/wp-admin/css/install.css @@ -16,16 +16,16 @@ body { } a { - color: #2271b1; + color: var(--wp-admin-theme-color); } a:hover, a:active { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20); } a:focus { - color: #043959; + color: var(--wp-admin-theme-color-darker-20); border-radius: 2px; box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 262b069e6da22..0e0e8801f0257 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3974,14 +3974,14 @@ function _default_wp_die_handler( $message, $title = '', $args = array() ) { font-size: 14px ; } a { - color: #2271b1; + color: #3858e9; } a:hover, a:active { - color: #135e96; + color: #183ad6; } a:focus { - color: #043959; + color: #183ad6; box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); outline: 2px solid transparent; } From bc0a300bc229cdc241b62519f20be8b0c9aeaa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Thu, 16 Apr 2026 08:16:54 +0000 Subject: [PATCH 20/57] Abilities API: Catch exceptions thrown by ability callbacks and return WP_Error. Wraps `invoke_callback()` in a try/catch so that exceptions thrown by execute or permission callbacks are converted to a `WP_Error` with the `ability_callback_exception` code instead of propagating as uncaught throwables. Developed in: https://github.com/WordPress/wordpress-develop/pull/11544 Reviewed by adamsilverstein, justlevine, jorbin. Merges [62238] to the 7.0 branch. Props priyankagusani, jamesgiroux, jeffpaul, dkotter, adamsilverstein, justlevine, jorbin, pavanpatil1. Fixes #65058. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62240 602fd350-edb4-49c9-b593-d223f7449a82 --- .../abilities-api/class-wp-ability.php | 16 ++++++- .../phpunit/tests/abilities-api/wpAbility.php | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index 967f1641156b0..cc01cc274c143 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -502,7 +502,7 @@ public function validate_input( $input = null ) { * * @param callable $callback The callable to invoke. * @param mixed $input Optional. The input data for the ability. Default `null`. - * @return mixed The result of the callable execution. + * @return mixed The result of the callable execution, or a `WP_Error` if the callback threw. */ protected function invoke_callback( callable $callback, $input = null ) { $args = array(); @@ -510,7 +510,19 @@ protected function invoke_callback( callable $callback, $input = null ) { $args[] = $input; } - return $callback( ...$args ); + try { + return $callback( ...$args ); + } catch ( Throwable $e ) { + return new WP_Error( + 'ability_callback_exception', + sprintf( + /* translators: 1: Ability name, 2: Exception message. */ + __( 'Ability "%1$s" callback threw an exception: %2$s' ), + esc_html( $this->name ), + esc_html( $e->getMessage() ) + ) + ); + } } /** diff --git a/tests/phpunit/tests/abilities-api/wpAbility.php b/tests/phpunit/tests/abilities-api/wpAbility.php index 73a5fbf17a9ef..aea2c09624929 100644 --- a/tests/phpunit/tests/abilities-api/wpAbility.php +++ b/tests/phpunit/tests/abilities-api/wpAbility.php @@ -497,6 +497,54 @@ public function test_execute_no_input() { $this->assertSame( 42, $ability->execute() ); } + /** + * Tests that an exception thrown by the execute callback is converted to a WP_Error + * instead of being propagated as an uncaught throwable. + * + * @ticket 65058 + */ + public function test_execute_catches_callback_exception() { + $args = array_merge( + self::$test_ability_properties, + array( + 'execute_callback' => static function (): int { + throw new RuntimeException( 'boom' ); + }, + ) + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $result = $ability->execute(); + + $this->assertWPError( $result, 'Ability::execute() should return WP_Error when the callback throws.' ); + $this->assertSame( 'ability_callback_exception', $result->get_error_code() ); + $this->assertStringContainsString( 'boom', $result->get_error_message() ); + } + + /** + * Tests that an exception thrown by the permission callback is converted to a WP_Error + * instead of being propagated as an uncaught throwable. + * + * @ticket 65058 + */ + public function test_check_permissions_catches_callback_exception() { + $args = array_merge( + self::$test_ability_properties, + array( + 'permission_callback' => static function (): bool { + throw new RuntimeException( 'permission exploded' ); + }, + ) + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $result = $ability->check_permissions(); + + $this->assertWPError( $result, 'Ability::check_permissions() should return WP_Error when the callback throws.' ); + $this->assertSame( 'ability_callback_exception', $result->get_error_code() ); + $this->assertStringContainsString( 'permission exploded', $result->get_error_message() ); + } + /** * Tests that before_execute_ability action is fired with correct parameters. * From 69a35b65bc3e6fd9c8780fb844a7339d6c85a5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Thu, 16 Apr 2026 08:20:06 +0000 Subject: [PATCH 21/57] AI: Prevent `wp_supports_ai` filter from overriding the `WP_AI_SUPPORT` constant. When `WP_AI_SUPPORT` is explicitly set to `false`, `wp_supports_ai()` now returns early before the filter runs. This ensures the site owner's explicit preference to disable AI cannot be overridden by a plugin via the `wp_supports_ai` filter. The filter default is now always `true`, since the constant check happens beforehand. Developed in: https://github.com/WordPress/wordpress-develop/pull/11295 Follow-up to [62067]. Reviewed by justlevine, westonruter, adamsilverstein. Merges [62239] to the 7.0 branch. Props justlevine, westonruter, gziolo, mindctrl, adamsilverstein, johnjamesjacoby, ahortin, nilambar, ozgursar, audrasjb, jeffpaul. Fixes #64706. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62241 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/ai-client.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/ai-client.php b/src/wp-includes/ai-client.php index 818e1dbaedcde..4fc20166fb8bb 100644 --- a/src/wp-includes/ai-client.php +++ b/src/wp-includes/ai-client.php @@ -17,20 +17,22 @@ * @return bool Whether AI features are supported. */ function wp_supports_ai(): bool { - $is_enabled = defined( 'WP_AI_SUPPORT' ) ? WP_AI_SUPPORT : true; + // Return early if AI is disabled by the current environment. + if ( defined( 'WP_AI_SUPPORT' ) && ! WP_AI_SUPPORT ) { + return false; + } /** - * Filters whether the current request should use AI. + * Filters whether the current request can use AI. * * This allows plugins and 3rd-party code to disable AI features on a per-request basis, or to even override explicit * preferences defined by the site owner. * * @since 7.0.0 * - * @param bool $is_enabled Whether the current request should use AI. Default to WP_AI_SUPPORT constant, or true if - * the constant is not defined. + * @param bool $is_enabled Whether AI is available. Default to true. */ - return (bool) apply_filters( 'wp_supports_ai', $is_enabled ); + return (bool) apply_filters( 'wp_supports_ai', true ); } /** From 8de9a149b0feedd2d5af00c6c0da17982d171d6e Mon Sep 17 00:00:00 2001 From: Aaron Jorbin Date: Thu, 16 Apr 2026 16:53:30 +0000 Subject: [PATCH 22/57] I18N: Add context for Next/Previous strings in the jQuery UI datepicker. Follow-up to [37849]. Reviewed by jorbin, audrasjb. Merges [62188] to the 7.0 branch. Props timse201, anupkankale, sergeyBiryukov. Fixes #65005. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62242 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/script-loader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index b80853f66429d..1a966bd3ac657 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -2020,8 +2020,8 @@ function wp_localize_jquery_ui_datepicker() { 'currentText' => __( 'Today' ), 'monthNames' => array_values( $wp_locale->month ), 'monthNamesShort' => array_values( $wp_locale->month_abbrev ), - 'nextText' => __( 'Next' ), - 'prevText' => __( 'Previous' ), + 'nextText' => _x( 'Next', 'datepicker: navigate to next month' ), + 'prevText' => _x( 'Previous', 'datepicker: navigate to previous month' ), 'dayNames' => array_values( $wp_locale->weekday ), 'dayNamesShort' => array_values( $wp_locale->weekday_abbrev ), 'dayNamesMin' => array_values( $wp_locale->weekday_initial ), From 5303b8562eda6125fd31043aae40bdde0166e47c Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 26 Apr 2026 23:57:25 +0000 Subject: [PATCH 23/57] Tests: Use `assertSame()` in `WP_AI_Client_Prompt_Builder` tests. This ensures that not only the return values match the expected results, but also that their type is the same. Going forward, stricter type checking by using `assertSame()` should generally be preferred to `assertEquals()` where appropriate, to make the tests more reliable. Follow-up to [61700]. Reviewed by peterwilsoncc. Merges r62248 to the 7.0 branch. Props sagardeshmukh, SergeyBiryukov. See #64324. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62265 602fd350-edb4-49c9-b593-d223f7449a82 --- .../ai-client/wpAiClientPromptBuilder.php | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 3630b0bab403a..3a781ccc73751 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -188,7 +188,7 @@ public function test_constructor_sets_default_request_timeout() { $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); $this->assertInstanceOf( RequestOptions::class, $request_options ); - $this->assertEquals( 30, $request_options->getTimeout() ); + $this->assertSame( 30.0, $request_options->getTimeout() ); } /** @@ -210,7 +210,7 @@ static function () { $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); $this->assertInstanceOf( RequestOptions::class, $request_options ); - $this->assertEquals( 45, $request_options->getTimeout() ); + $this->assertSame( 45.0, $request_options->getTimeout() ); } /** @@ -401,7 +401,7 @@ public function test_constructor_with_string_prompt() { $this->assertCount( 1, $messages ); $this->assertInstanceOf( Message::class, $messages[0] ); - $this->assertEquals( 'Hello, world!', $messages[0]->getParts()[0]->getText() ); + $this->assertSame( 'Hello, world!', $messages[0]->getParts()[0]->getText() ); } /** @@ -418,7 +418,7 @@ public function test_constructor_with_message_part_prompt() { $this->assertCount( 1, $messages ); $this->assertInstanceOf( Message::class, $messages[0] ); - $this->assertEquals( 'Test message', $messages[0]->getParts()[0]->getText() ); + $this->assertSame( 'Test message', $messages[0]->getParts()[0]->getText() ); } /** @@ -479,7 +479,7 @@ public function test_constructor_with_message_array_shape() { $this->assertCount( 1, $messages ); $this->assertInstanceOf( Message::class, $messages[0] ); - $this->assertEquals( 'Hello from array', $messages[0]->getParts()[0]->getText() ); + $this->assertSame( 'Hello from array', $messages[0]->getParts()[0]->getText() ); } /** @@ -497,7 +497,7 @@ public function test_with_text() { $messages = $this->get_wrapped_prompt_builder_property_value( $builder, 'messages' ); $this->assertCount( 1, $messages ); - $this->assertEquals( 'Some text', $messages[0]->getParts()[0]->getText() ); + $this->assertSame( 'Some text', $messages[0]->getParts()[0]->getText() ); } /** @@ -515,8 +515,8 @@ public function test_with_text_appends_to_existing_user_message() { $this->assertCount( 1, $messages ); $parts = $messages[0]->getParts(); $this->assertCount( 2, $parts ); - $this->assertEquals( 'Initial text', $parts[0]->getText() ); - $this->assertEquals( ' Additional text', $parts[1]->getText() ); + $this->assertSame( 'Initial text', $parts[0]->getText() ); + $this->assertSame( ' Additional text', $parts[1]->getText() ); } /** @@ -537,8 +537,8 @@ public function test_with_inline_file() { $this->assertCount( 1, $messages ); $file = $messages[0]->getParts()[0]->getFile(); $this->assertInstanceOf( File::class, $file ); - $this->assertEquals( 'data:image/png;base64,' . $base64, $file->getDataUri() ); - $this->assertEquals( 'image/png', $file->getMimeType() ); + $this->assertSame( 'data:image/png;base64,' . $base64, $file->getDataUri() ); + $this->assertSame( 'image/png', $file->getMimeType() ); } /** @@ -558,8 +558,8 @@ public function test_with_remote_file() { $this->assertCount( 1, $messages ); $file = $messages[0]->getParts()[0]->getFile(); $this->assertInstanceOf( File::class, $file ); - $this->assertEquals( 'https://example.com/image.jpg', $file->getUrl() ); - $this->assertEquals( 'image/jpeg', $file->getMimeType() ); + $this->assertSame( 'https://example.com/image.jpg', $file->getUrl() ); + $this->assertSame( 'image/jpeg', $file->getMimeType() ); } /** @@ -580,7 +580,7 @@ public function test_with_inline_file_data_uri() { $this->assertCount( 1, $messages ); $file = $messages[0]->getParts()[0]->getFile(); $this->assertInstanceOf( File::class, $file ); - $this->assertEquals( 'image/jpeg', $file->getMimeType() ); + $this->assertSame( 'image/jpeg', $file->getMimeType() ); } /** @@ -600,8 +600,8 @@ public function test_with_remote_file_without_mime_type() { $this->assertCount( 1, $messages ); $file = $messages[0]->getParts()[0]->getFile(); $this->assertInstanceOf( File::class, $file ); - $this->assertEquals( 'https://example.com/audio.mp3', $file->getUrl() ); - $this->assertEquals( 'audio/mpeg', $file->getMimeType() ); + $this->assertSame( 'https://example.com/audio.mp3', $file->getUrl() ); + $this->assertSame( 'audio/mpeg', $file->getMimeType() ); } /** @@ -644,9 +644,9 @@ public function test_with_message_parts() { $this->assertCount( 1, $messages ); $parts = $messages[0]->getParts(); $this->assertCount( 3, $parts ); - $this->assertEquals( 'Part 1', $parts[0]->getText() ); - $this->assertEquals( 'Part 2', $parts[1]->getText() ); - $this->assertEquals( 'Part 3', $parts[2]->getText() ); + $this->assertSame( 'Part 1', $parts[0]->getText() ); + $this->assertSame( 'Part 2', $parts[1]->getText() ); + $this->assertSame( 'Part 3', $parts[2]->getText() ); } /** @@ -670,9 +670,9 @@ public function test_with_history() { $messages = $this->get_wrapped_prompt_builder_property_value( $builder, 'messages' ); $this->assertCount( 3, $messages ); - $this->assertEquals( 'User 1', $messages[0]->getParts()[0]->getText() ); - $this->assertEquals( 'Model 1', $messages[1]->getParts()[0]->getText() ); - $this->assertEquals( 'User 2', $messages[2]->getParts()[0]->getText() ); + $this->assertSame( 'User 1', $messages[0]->getParts()[0]->getText() ); + $this->assertSame( 'Model 1', $messages[1]->getParts()[0]->getText() ); + $this->assertSame( 'User 2', $messages[2]->getParts()[0]->getText() ); } /** @@ -710,9 +710,9 @@ public function test_constructor_with_string_parts_list() { $this->assertInstanceOf( Message::class, $messages[0] ); $parts = $messages[0]->getParts(); $this->assertCount( 3, $parts ); - $this->assertEquals( 'Part 1', $parts[0]->getText() ); - $this->assertEquals( 'Part 2', $parts[1]->getText() ); - $this->assertEquals( 'Part 3', $parts[2]->getText() ); + $this->assertSame( 'Part 1', $parts[0]->getText() ); + $this->assertSame( 'Part 2', $parts[1]->getText() ); + $this->assertSame( 'Part 3', $parts[2]->getText() ); } /** @@ -735,9 +735,9 @@ public function test_constructor_with_mixed_parts_list() { $this->assertCount( 1, $messages ); $parts = $messages[0]->getParts(); $this->assertCount( 3, $parts ); - $this->assertEquals( 'String part', $parts[0]->getText() ); - $this->assertEquals( 'Part 1', $parts[1]->getText() ); - $this->assertEquals( 'Part 2', $parts[2]->getText() ); + $this->assertSame( 'String part', $parts[0]->getText() ); + $this->assertSame( 'Part 1', $parts[1]->getText() ); + $this->assertSame( 'Part 2', $parts[2]->getText() ); } /** @@ -775,13 +775,13 @@ public function test_method_chaining() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'Be helpful', $config->getSystemInstruction() ); - $this->assertEquals( 500, $config->getMaxTokens() ); - $this->assertEquals( 0.8, $config->getTemperature() ); - $this->assertEquals( 0.95, $config->getTopP() ); - $this->assertEquals( 50, $config->getTopK() ); - $this->assertEquals( 2, $config->getCandidateCount() ); - $this->assertEquals( 'application/json', $config->getOutputMimeType() ); + $this->assertSame( 'Be helpful', $config->getSystemInstruction() ); + $this->assertSame( 500, $config->getMaxTokens() ); + $this->assertSame( 0.8, $config->getTemperature() ); + $this->assertSame( 0.95, $config->getTopP() ); + $this->assertSame( 50, $config->getTopK() ); + $this->assertSame( 2, $config->getCandidateCount() ); + $this->assertSame( 'application/json', $config->getOutputMimeType() ); } /** @@ -1001,11 +1001,11 @@ public function test_using_model_config() { /** @var ModelConfig $merged_config */ $merged_config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'Builder instruction', $merged_config->getSystemInstruction() ); - $this->assertEquals( 500, $merged_config->getMaxTokens() ); - $this->assertEquals( 0.5, $merged_config->getTemperature() ); - $this->assertEquals( 0.9, $merged_config->getTopP() ); - $this->assertEquals( 40, $merged_config->getTopK() ); + $this->assertSame( 'Builder instruction', $merged_config->getSystemInstruction() ); + $this->assertSame( 500, $merged_config->getMaxTokens() ); + $this->assertSame( 0.5, $merged_config->getTemperature() ); + $this->assertSame( 0.9, $merged_config->getTopP() ); + $this->assertSame( 40, $merged_config->getTopK() ); } /** @@ -1028,22 +1028,22 @@ public function test_using_model_config_with_custom_options() { $this->assertArrayHasKey( 'stopSequences', $custom_options ); $this->assertIsArray( $custom_options['stopSequences'] ); - $this->assertEquals( array( 'CONFIG_STOP' ), $custom_options['stopSequences'] ); + $this->assertSame( array( 'CONFIG_STOP' ), $custom_options['stopSequences'] ); $this->assertArrayHasKey( 'otherOption', $custom_options ); - $this->assertEquals( 'value', $custom_options['otherOption'] ); + $this->assertSame( 'value', $custom_options['otherOption'] ); $builder->using_stop_sequences( 'STOP' ); /** @var ModelConfig $merged_config */ $merged_config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( array( 'STOP' ), $merged_config->getStopSequences() ); + $this->assertSame( array( 'STOP' ), $merged_config->getStopSequences() ); $custom_options = $merged_config->getCustomOptions(); $this->assertArrayHasKey( 'stopSequences', $custom_options ); - $this->assertEquals( array( 'CONFIG_STOP' ), $custom_options['stopSequences'] ); + $this->assertSame( array( 'CONFIG_STOP' ), $custom_options['stopSequences'] ); $this->assertArrayHasKey( 'otherOption', $custom_options ); - $this->assertEquals( 'value', $custom_options['otherOption'] ); + $this->assertSame( 'value', $custom_options['otherOption'] ); } /** @@ -1058,7 +1058,7 @@ public function test_using_provider() { $this->assertSame( $builder, $result ); $actual_provider = $this->get_wrapped_prompt_builder_property_value( $builder, 'providerIdOrClassName' ); - $this->assertEquals( 'test-provider', $actual_provider ); + $this->assertSame( 'test-provider', $actual_provider ); } /** @@ -1075,7 +1075,7 @@ public function test_using_system_instruction() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'You are a helpful assistant.', $config->getSystemInstruction() ); + $this->assertSame( 'You are a helpful assistant.', $config->getSystemInstruction() ); } /** @@ -1092,7 +1092,7 @@ public function test_using_max_tokens() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 1000, $config->getMaxTokens() ); + $this->assertSame( 1000, $config->getMaxTokens() ); } /** @@ -1109,7 +1109,7 @@ public function test_using_temperature() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 0.7, $config->getTemperature() ); + $this->assertSame( 0.7, $config->getTemperature() ); } /** @@ -1126,7 +1126,7 @@ public function test_using_top_p() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 0.9, $config->getTopP() ); + $this->assertSame( 0.9, $config->getTopP() ); } /** @@ -1143,7 +1143,7 @@ public function test_using_top_k() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 40, $config->getTopK() ); + $this->assertSame( 40, $config->getTopK() ); } /** @@ -1160,7 +1160,7 @@ public function test_using_stop_sequences() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( array( 'STOP', 'END', '###' ), $config->getStopSequences() ); + $this->assertSame( array( 'STOP', 'END', '###' ), $config->getStopSequences() ); } /** @@ -1177,7 +1177,7 @@ public function test_using_candidate_count() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 3, $config->getCandidateCount() ); + $this->assertSame( 3, $config->getCandidateCount() ); } /** @@ -1194,7 +1194,7 @@ public function test_using_output_mime() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'application/json', $config->getOutputMimeType() ); + $this->assertSame( 'application/json', $config->getOutputMimeType() ); } /** @@ -1218,7 +1218,7 @@ public function test_using_output_schema() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( $schema, $config->getOutputSchema() ); + $this->assertSame( $schema, $config->getOutputSchema() ); } /** @@ -1258,7 +1258,7 @@ public function test_as_json_response() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'application/json', $config->getOutputMimeType() ); + $this->assertSame( 'application/json', $config->getOutputMimeType() ); } /** @@ -1276,8 +1276,8 @@ public function test_as_json_response_with_schema() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'application/json', $config->getOutputMimeType() ); - $this->assertEquals( $schema, $config->getOutputSchema() ); + $this->assertSame( 'application/json', $config->getOutputMimeType() ); + $this->assertSame( $schema, $config->getOutputSchema() ); } /** @@ -1824,14 +1824,14 @@ public function test_generate_texts() { $texts = $builder->generate_texts( 3 ); $this->assertCount( 3, $texts ); - $this->assertEquals( 'Text 1', $texts[0] ); - $this->assertEquals( 'Text 2', $texts[1] ); - $this->assertEquals( 'Text 3', $texts[2] ); + $this->assertSame( 'Text 1', $texts[0] ); + $this->assertSame( 'Text 2', $texts[1] ); + $this->assertSame( 'Text 3', $texts[2] ); /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 3, $config->getCandidateCount() ); + $this->assertSame( 3, $config->getCandidateCount() ); } /** @@ -2255,7 +2255,7 @@ public function test_as_output_media_aspect_ratio() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( '16:9', $config->getOutputMediaAspectRatio() ); + $this->assertSame( '16:9', $config->getOutputMediaAspectRatio() ); } /** @@ -2272,7 +2272,7 @@ public function test_as_output_speech_voice() { /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'alloy', $config->getOutputSpeechVoice() ); + $this->assertSame( 'alloy', $config->getOutputSpeechVoice() ); } /** @@ -2290,8 +2290,8 @@ public function test_using_ability_with_string() { $this->assertNotNull( $declarations ); $this->assertCount( 1, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); - $this->assertEquals( 'A simple test ability with no parameters.', $declarations[0]->getDescription() ); + $this->assertSame( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); + $this->assertSame( 'A simple test ability with no parameters.', $declarations[0]->getDescription() ); } /** @@ -2311,8 +2311,8 @@ public function test_using_ability_with_wp_ability_object() { $this->assertNotNull( $declarations ); $this->assertCount( 1, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__with-params', $declarations[0]->getName() ); - $this->assertEquals( 'A test ability that accepts parameters.', $declarations[0]->getDescription() ); + $this->assertSame( 'wpab__wpaiclienttests__with-params', $declarations[0]->getName() ); + $this->assertSame( 'A test ability that accepts parameters.', $declarations[0]->getDescription() ); $params = $declarations[0]->getParameters(); $this->assertNotNull( $params ); @@ -2339,9 +2339,9 @@ public function test_using_ability_with_multiple_abilities() { $this->assertNotNull( $declarations ); $this->assertCount( 3, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); - $this->assertEquals( 'wpab__wpaiclienttests__with-params', $declarations[1]->getName() ); - $this->assertEquals( 'wpab__wpaiclienttests__returns-error', $declarations[2]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__with-params', $declarations[1]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__returns-error', $declarations[2]->getName() ); } /** @@ -2367,8 +2367,8 @@ public function test_using_ability_skips_nonexistent_abilities() { $this->assertNotNull( $declarations ); $this->assertCount( 2, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); - $this->assertEquals( 'wpab__wpaiclienttests__with-params', $declarations[1]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__with-params', $declarations[1]->getName() ); } /** @@ -2407,8 +2407,8 @@ public function test_using_ability_with_mixed_types() { $this->assertNotNull( $declarations ); $this->assertCount( 2, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); - $this->assertEquals( 'wpab__wpaiclienttests__with-params', $declarations[1]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__with-params', $declarations[1]->getName() ); } /** @@ -2426,7 +2426,7 @@ public function test_using_ability_with_hyphenated_name() { $this->assertNotNull( $declarations ); $this->assertCount( 1, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__hyphen-test', $declarations[0]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__hyphen-test', $declarations[0]->getName() ); } /** @@ -2448,13 +2448,13 @@ public function test_using_ability_method_chaining() { $this->assertNotNull( $declarations ); $this->assertCount( 1, $declarations ); - $this->assertEquals( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); + $this->assertSame( 'wpab__wpaiclienttests__simple', $declarations[0]->getName() ); /** @var ModelConfig $config */ $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); - $this->assertEquals( 'You are a helpful assistant', $config->getSystemInstruction() ); - $this->assertEquals( 500, $config->getMaxTokens() ); + $this->assertSame( 'You are a helpful assistant', $config->getSystemInstruction() ); + $this->assertSame( 500, $config->getMaxTokens() ); } /** From d615e1748426d07cd696cfa32e345bd487a47a99 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Sun, 26 Apr 2026 23:59:30 +0000 Subject: [PATCH 24/57] AI: Validate filtered default request timeout in `WP_AI_Client_Prompt_Builder`. This checks that the return value of the `wp_ai_client_default_request_timeout` filter is a non-negative number before passing it to `RequestOptions`. If the filtered value is invalid, it is discarded in favor of the original default of `30.0` and a `_doing_it_wrong()` notice is issued. Without this check, a fatal error would ensue from the exception thrown in `\WordPress\AiClient\Providers\Http\DTO\RequestOptions::validateTimeout()`. The following static analysis issues are addressed: * Use `float` instead of `int` for the `wp_ai_client_default_request_timeout` filter parameter. * Add missing PHP imports for `Message` and `MessagePart` in the PHPDoc for `wp_ai_client_prompt()`. * Add PHP return type hints for `wp_ai_client_prompt()` and `WP_AI_Client_Cache::getMultiple()`. * Use native property type hints in `WP_AI_Client_HTTP_Client`. Developed in https://github.com/WordPress/wordpress-develop/pull/11596 Reviewed by peterwilsoncc. Merges r62255 to the 7.0 branch. Props westonruter, justlevine, flixos90, khushdoms, darshitrajyaguru97, adrmf25, jarodortegaaraya, tusharaddweb, gaurangsondagar. Fixes #65094. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62266 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/ai-client.php | 4 +- .../adapters/class-wp-ai-client-cache.php | 2 +- .../class-wp-ai-client-http-client.php | 6 +- .../class-wp-ai-client-prompt-builder.php | 19 ++++- .../ai-client/wpAiClientPromptBuilder.php | 72 +++++++++++++++++-- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/src/wp-includes/ai-client.php b/src/wp-includes/ai-client.php index 4fc20166fb8bb..b38c7b721416d 100644 --- a/src/wp-includes/ai-client.php +++ b/src/wp-includes/ai-client.php @@ -8,6 +8,8 @@ */ use WordPress\AiClient\AiClient; +use WordPress\AiClient\Messages\DTO\Message; +use WordPress\AiClient\Messages\DTO\MessagePart; /** * Returns whether AI features are supported in the current environment. @@ -55,6 +57,6 @@ function wp_supports_ai(): bool { * conversations. Default null. * @return WP_AI_Client_Prompt_Builder The prompt builder instance. */ -function wp_ai_client_prompt( $prompt = null ) { +function wp_ai_client_prompt( $prompt = null ): WP_AI_Client_Prompt_Builder { return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt ); } diff --git a/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php b/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php index 18d85eee6c9e6..45504897485f7 100644 --- a/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php +++ b/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php @@ -104,7 +104,7 @@ public function clear(): bool { * @param mixed $default_value Default value to return for keys that do not exist. * @return array A list of key => value pairs. */ - public function getMultiple( $keys, $default_value = null ) { + public function getMultiple( $keys, $default_value = null ): array { /** * Keys array. * diff --git a/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php b/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php index f1827db0e437c..f6c6dea441d1c 100644 --- a/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php +++ b/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php @@ -32,17 +32,15 @@ class WP_AI_Client_HTTP_Client implements ClientInterface, ClientWithOptionsInte * Response factory instance. * * @since 7.0.0 - * @var ResponseFactoryInterface */ - private $response_factory; + private ResponseFactoryInterface $response_factory; /** * Stream factory instance. * * @since 7.0.0 - * @var StreamFactoryInterface */ - private $stream_factory; + private StreamFactoryInterface $stream_factory; /** * Constructor. diff --git a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php index d1f2271bd47d3..da7858dd76555 100644 --- a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php +++ b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php @@ -190,14 +190,29 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) { $this->error = $this->exception_to_wp_error( $e ); } + $default_timeout = 30.0; + /** * Filters the default request timeout in seconds for AI Client HTTP requests. * * @since 7.0.0 * - * @param int $default_timeout The default timeout in seconds. + * @param float $default_timeout The default timeout in seconds. */ - $default_timeout = (int) apply_filters( 'wp_ai_client_default_request_timeout', 30 ); + $filtered_default_timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout ); + if ( is_numeric( $filtered_default_timeout ) && (float) $filtered_default_timeout >= 0.0 ) { + $default_timeout = (float) $filtered_default_timeout; + } else { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: wp_ai_client_default_request_timeout */ + __( 'The %s filter must return a non-negative number.' ), + 'wp_ai_client_default_request_timeout' + ), + '7.0.0' + ); + } $this->builder->usingRequestOptions( RequestOptions::fromArray( diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 3a781ccc73751..e758a6868aa42 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -192,15 +192,64 @@ public function test_constructor_sets_default_request_timeout() { } /** - * Test that the constructor allows overriding the default request timeout. + * Test that the constructor allows overriding the default request timeout with a valid value. * * @ticket 64591 + * @ticket 65094 + * + * @dataProvider data_valid_request_timeout_overrides + * + * @param mixed $input The timeout value returned by the filter. + * @param float $expected The expected timeout stored on the request options. + */ + public function test_constructor_allows_overriding_request_timeout_with_valid_timeout( $input, float $expected ) { + add_filter( + 'wp_ai_client_default_request_timeout', + static function () use ( $input ) { + return $input; + } + ); + + $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); + + /** @var RequestOptions $request_options */ + $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); + + $this->assertInstanceOf( RequestOptions::class, $request_options ); + $this->assertSame( $expected, $request_options->getTimeout() ); + } + + /** + * Data provider for {@see self::test_constructor_allows_overriding_request_timeout_with_valid_timeout()}. + * + * @return array */ - public function test_constructor_allows_overriding_request_timeout() { + public function data_valid_request_timeout_overrides(): array { + return array( + 'float' => array( 45.5, 45.5 ), + 'integer' => array( 67, 67.0 ), + 'string' => array( '20', 20.0 ), + 'infinity' => array( INF, INF ), + 'zero' => array( 0.0, 0.0 ), + ); + } + + /** + * Test that the constructor disallows overriding the default request timeout with an invalid value. + * + * @ticket 65094 + * + * @dataProvider data_invalid_request_timeouts + * + * @expectedIncorrectUsage WP_AI_Client_Prompt_Builder::__construct + * + * @param mixed $timeout The invalid timeout value returned by the filter. + */ + public function test_constructor_disallows_overriding_with_invalid_request_timeout( $timeout ) { add_filter( 'wp_ai_client_default_request_timeout', - static function () { - return 45; + static function () use ( $timeout ) { + return $timeout; } ); @@ -210,7 +259,20 @@ static function () { $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); $this->assertInstanceOf( RequestOptions::class, $request_options ); - $this->assertSame( 45.0, $request_options->getTimeout() ); + $this->assertSame( 30.0, $request_options->getTimeout() ); + } + + /** + * Data provider for {@see self::test_constructor_disallows_overriding_with_invalid_request_timeout()}. + * + * @return array + */ + public function data_invalid_request_timeouts(): array { + return array( + 'negative number' => array( -1 ), + 'array' => array( array() ), + 'null' => array( null ), + ); } /** From 77a59c8b549435b7848d891e909c15eaa53b43b7 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 27 Apr 2026 00:03:37 +0000 Subject: [PATCH 25/57] I18N: Add context for the Library admin menu item. Reviewed by peterwilsoncc. Merges r62256 to the 7.0 branch. Props timse201, sanketparmar, trickster301, audrasjb, jadavsanjay, SergeyBiryukov. Fixes #64982. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62267 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/menu.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/menu.php b/src/wp-admin/menu.php index dc8c4271e9aad..57d94c75e26f2 100644 --- a/src/wp-admin/menu.php +++ b/src/wp-admin/menu.php @@ -72,7 +72,7 @@ $menu[10] = array( __( 'Media' ), 'upload_files', 'upload.php', '', 'menu-top menu-icon-media', 'menu-media', 'dashicons-admin-media' ); - $submenu['upload.php'][5] = array( __( 'Library' ), 'upload_files', 'upload.php' ); + $submenu['upload.php'][5] = array( _x( 'Library', 'media library menu item' ), 'upload_files', 'upload.php' ); $submenu['upload.php'][10] = array( __( 'Add Media File' ), 'upload_files', 'media-new.php' ); $submenu_index = 15; From 762320faa5079ed41505fd210f5b37fcae355cfc Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 27 Apr 2026 02:19:27 +0000 Subject: [PATCH 26/57] Administration: Widen screen options number of items field. Modifies the Screen Options > Number of items per page field to avoid cropping of three digit numbers when setting a list view to display a lot of items. Reviewed by audrasjb. Merges r62268 to the 7.0 branch. Props apermo, audrasjb, darshitrajyaguru97, ekla, gaurangsondagar, jigarkahar, juanmaguitar, khushdoms, peterwilsoncc, sabernhardt, shailu25, shatrumyatra, yusufmudagal. Fixes #65104. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62269 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-screen.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-screen.php b/src/wp-admin/includes/class-wp-screen.php index 838f0795cd5d3..227fda90c6273 100644 --- a/src/wp-admin/includes/class-wp-screen.php +++ b/src/wp-admin/includes/class-wp-screen.php @@ -1280,7 +1280,7 @@ public function render_per_page_options() { - From 6793ee106a0bba858e96bde87a37be0d4dcbf90b Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 27 Apr 2026 02:23:13 +0000 Subject: [PATCH 27/57] Administration: Resize classic editor slug field for new theme. Reduces the size and improves the alignment of the post slug field following the re-design of form elements as part of the new admin theme. Reviewed by wildworks. Merges r62263 to the 7.0 branch. Props wildworks, sabernhardt, audrasjb, dhruvang21, shailu25, joedolson, khushdoms, tusharaddweb. Fixes #65063. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62270 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/_enqueues/admin/post.js | 2 +- src/wp-admin/css/edit.css | 7 +++++-- src/wp-includes/css/buttons.css | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/js/_enqueues/admin/post.js b/src/js/_enqueues/admin/post.js index 1dde10ffc8aae..d50fe6007d33b 100644 --- a/src/js/_enqueues/admin/post.js +++ b/src/js/_enqueues/admin/post.js @@ -1028,7 +1028,7 @@ jQuery( function($) { revert_e = $el.html(); buttons.html( - ' ' + + ' ' + '' ); diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index b98dd889c59fe..133616d335a6d 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -121,7 +121,6 @@ input#link_url { #edit-slug-box .cancel { margin-right: 10px; padding: 0; - font-size: 11px; } #comment-link-box { @@ -140,7 +139,7 @@ input#link_url { #editable-post-name input { font-size: 13px; font-weight: 400; - height: 24px; + min-height: 32px; margin: 0; width: 16em; } @@ -1068,6 +1067,10 @@ form#tags-filter { #edit-slug-box { padding: 0; } + + #editable-post-name input { + min-height: 40px; + } } @media only screen and (max-width: 1004px) { diff --git a/src/wp-includes/css/buttons.css b/src/wp-includes/css/buttons.css index e092764121b11..cb6e18dbffcb8 100644 --- a/src/wp-includes/css/buttons.css +++ b/src/wp-includes/css/buttons.css @@ -380,6 +380,7 @@ TABLE OF CONTENTS: .wp-core-ui .button, .wp-core-ui .button.button-large, + .wp-core-ui .button.button-compact, .wp-core-ui .button.button-small, input#publish, input#save-post, From 4749d95b5b83416824e3b2b0b63a599eaea43c0c Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 27 Apr 2026 06:11:07 +0000 Subject: [PATCH 28/57] Administration: Fix misaligned icon in user profile password field. This changeset corrects a misalignment issue affecting the show/hide button next to the password field. Reviewed by peterwilsoncc. Merges [62262] and [62272] to the 7.0 branch. Props piyushpatel123, rajdiptank111, ankitkumarshah, andrewssanya, jdahir0789, gautammkgarg, gaurangsondagar, gaisma22, ugyensupport, abduremon, ankitmaru, darshitrajyaguru97, khushdoms, monzuralam, mukesh27, wildworks. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62274 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/forms.css | 6 ++++++ src/wp-admin/user-edit.php | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index e4e09ca1b6023..b48825a1ef5a3 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -583,6 +583,12 @@ input[type="number"].tiny-text { vertical-align: middle; } +.button.wp-hide-pw.user-new-password-toggle { + display: inline-flex; + align-items: center; + column-gap: 4px; +} + .wp-cancel-pw .dashicons-no { display: none; } diff --git a/src/wp-admin/user-edit.php b/src/wp-admin/user-edit.php index cfad6afbab8dc..c25380a93ee91 100644 --- a/src/wp-admin/user-edit.php +++ b/src/wp-admin/user-edit.php @@ -695,7 +695,7 @@ - From 4bca8595cbde742ceb2946834684c993fd0f754a Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Mon, 27 Apr 2026 13:48:31 +0000 Subject: [PATCH 29/57] Build/Test Tools: Copy vendor scripts earlier in the build. Relocates the copy-vendor-scripts to run during the the build:js portion of the build script. This ensures the JavaScript files are in place before the uglify:all task is run. Follow up to r61438. Reviewed by desrosj, peterwilsoncc. Merges [62189] to the 7.0 branch. Props desrosj, peterwilsoncc. Fixes #65006. See #64393. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62275 602fd350-edb4-49c9-b593-d223f7449a82 --- Gruntfile.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 1c4280aff213b..5f9109fac3cb0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1837,6 +1837,7 @@ module.exports = function(grunt) { 'clean:js', 'build:webpack', 'copy:js', + 'copy-vendor-scripts', 'file_append', 'uglify:all', 'concat:tinymce', @@ -2133,7 +2134,6 @@ module.exports = function(grunt) { 'build:css', 'build:codemirror', 'build:gutenberg', - 'copy-vendor-scripts', 'build:certificates' ] ); } else { @@ -2145,7 +2145,6 @@ module.exports = function(grunt) { 'build:css', 'build:codemirror', 'build:gutenberg', - 'copy-vendor-scripts', 'replace:source-maps', 'verify:build' ] ); From 9542580086221abcc95f93daec1cf38a0cc98b30 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Mon, 27 Apr 2026 13:51:15 +0000 Subject: [PATCH 30/57] Build/Test Tools: Consolidate vendor file copying to ensure `.min.js` files are minified. Relocates the copying of vendor JavaScript files back to the `grunt copy:vendor-js` subtask to ensure the files are in place prior to the grunt uglify step running to minify the files. Reviewed by desrosj, peterwilsoncc. Merges [62258] to the 7.0 branch. Props desrosj, peterwilsoncc. Fixes #65007. See #64393. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62276 602fd350-edb4-49c9-b593-d223f7449a82 --- Gruntfile.js | 76 +++++++++++--- package.json | 3 +- tools/vendors/copy-vendors.js | 185 ---------------------------------- 3 files changed, 63 insertions(+), 201 deletions(-) delete mode 100644 tools/vendors/copy-vendors.js diff --git a/Gruntfile.js b/Gruntfile.js index 5f9109fac3cb0..8863d030627b8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -397,6 +397,55 @@ module.exports = function(grunt) { 'suggest*' ], dest: WORKING_DIR + 'wp-includes/js/jquery/' + }, + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/lodash.js' ]: [ './node_modules/lodash/lodash.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/lodash.min.js' ]: [ './node_modules/lodash/lodash.min.js' ], + }, + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/moment.js' ]: [ './node_modules/moment/moment.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/moment.min.js' ]: [ './node_modules/moment/min/moment.min.js' ], + }, + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.js' ]: [ './node_modules/regenerator-runtime/runtime.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.min.js' ]: [ './node_modules/regenerator-runtime/runtime.js' ], + }, + // React libraries: react, react-dom + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react.js' ]: [ './node_modules/react/umd/react.development.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react.min.js' ]: [ './node_modules/react/umd/react.production.min.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react-dom.js' ]: [ './node_modules/react-dom/umd/react-dom.development.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react-dom.min.js' ]: [ './node_modules/react-dom/umd/react-dom.production.min.js' ], + }, + // Polyfills + { + // @wordpress/babel-preset-default + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill.js' ]: [ './node_modules/@wordpress/babel-preset-default/build/polyfill.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill.min.js' ]: [ './node_modules/@wordpress/babel-preset-default/build/polyfill.min.js' ], + // polyfill-library (DOMRect) + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-dom-rect.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/DOMRect/raw.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-dom-rect.min.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/DOMRect/min.js' ], + // element-closest + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-element-closest.js' ]: [ './node_modules/element-closest/browser.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-element-closest.min.js' ]: [ './node_modules/element-closest/browser.js' ], + // whatwg-fetch + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.js' ]: [ './node_modules/whatwg-fetch/dist/fetch.umd.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.min.js' ]: [ './node_modules/whatwg-fetch/dist/fetch.umd.js' ], + // formdata-polyfill + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-formdata.js' ]: [ './node_modules/formdata-polyfill/FormData.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-formdata.min.js' ]: [ './node_modules/formdata-polyfill/formdata.min.js' ], + // wicg-inert + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-inert.js' ]: [ './node_modules/wicg-inert/dist/inert.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-inert.min.js' ]: [ './node_modules/wicg-inert/dist/inert.min.js' ], + // polyfill-library (Node.prototype.contains) + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-node-contains.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/Node.prototype.contains/raw.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-node-contains.min.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/Node.prototype.contains/min.js' ], + // objectFitPolyfill + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-object-fit.js' ]: [ './node_modules/objectFitPolyfill/src/objectFitPolyfill.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-object-fit.min.js' ]: [ './node_modules/objectFitPolyfill/dist/objectFitPolyfill.min.js' ], + // core-js-url-browser + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-url.js' ]: [ './node_modules/core-js-url-browser/url.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-url.min.js' ]: [ './node_modules/core-js-url-browser/url.min.js' ], } ].concat( // Copy tinymce.js only when building to /src. @@ -1107,6 +1156,14 @@ module.exports = function(grunt) { src: WORKING_DIR + 'wp-includes/js/dist/vendor/moment.js', dest: WORKING_DIR + 'wp-includes/js/dist/vendor/moment.min.js' }, + 'regenerator-runtime': { + src: WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.js', + dest: WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.min.js' + }, + 'wp-polyfill-fetch': { + src: WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.js', + dest: WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.min.js' + }, dynamic: { expand: true, cwd: WORKING_DIR, @@ -1635,18 +1692,6 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'copy-vendor-scripts', 'Copies vendor scripts from node_modules to wp-includes/js/dist/vendor/.', function() { - const done = this.async(); - const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; - grunt.util.spawn( { - cmd: 'node', - args: [ 'tools/vendors/copy-vendors.js', `--build-dir=${ buildDir }` ], - opts: { stdio: 'inherit' } - }, function( error ) { - done( ! error ); - } ); - } ); - grunt.renameTask( 'watch', '_watch' ); grunt.registerTask( 'watch', function() { @@ -1675,6 +1720,8 @@ module.exports = function(grunt) { 'uglify:imgareaselect', 'uglify:jqueryform', 'uglify:moment', + 'uglify:regenerator-runtime', + 'uglify:wp-polyfill-fetch', 'qunit:compiled' ] ); @@ -1817,7 +1864,9 @@ module.exports = function(grunt) { 'uglify:jquery-ui', 'uglify:imgareaselect', 'uglify:jqueryform', - 'uglify:moment' + 'uglify:moment', + 'uglify:regenerator-runtime', + 'uglify:wp-polyfill-fetch' ] ); grunt.registerTask( 'build:codemirror', [ @@ -1837,7 +1886,6 @@ module.exports = function(grunt) { 'clean:js', 'build:webpack', 'copy:js', - 'copy-vendor-scripts', 'file_append', 'uglify:all', 'concat:tinymce', diff --git a/package.json b/package.json index 731ab3c87597c..a810bf34bbe98 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,6 @@ "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", "gutenberg:copy": "node tools/gutenberg/copy.js", "gutenberg:verify": "node tools/gutenberg/utils.js", - "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg --dev", - "vendor:copy": "node tools/vendors/copy-vendors.js" + "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg --dev" } } diff --git a/tools/vendors/copy-vendors.js b/tools/vendors/copy-vendors.js deleted file mode 100644 index 12660fc639645..0000000000000 --- a/tools/vendors/copy-vendors.js +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env node - -/** - * Copy Vendor Scripts - * - * This script copies vendor dependencies from node_modules to wp-includes/js/dist/vendor/. - * These are Core's own dependencies (moment, lodash, regenerator-runtime, polyfills, etc.) - * separate from Gutenberg packages. - * - * @package WordPress - */ - -const fs = require( 'fs' ); -const path = require( 'path' ); - -// Paths -const rootDir = path.resolve( __dirname, '../..' ); -const nodeModulesDir = path.join( rootDir, 'node_modules' ); - -// Parse command line arguments -const args = process.argv.slice( 2 ); -const buildDirArg = args.find( arg => arg.startsWith( '--build-dir=' ) ); -const buildTarget = buildDirArg - ? buildDirArg.split( '=' )[1] - : ( args.includes( '--dev' ) ? 'src' : 'build' ); - -const vendorDir = path.join( rootDir, buildTarget, 'wp-includes/js/dist/vendor' ); - -/** - * Vendor files to copy from node_modules. - */ -const VENDOR_FILES = { - // Moment.js - 'moment': { - files: [ - { from: 'moment/moment.js', to: 'moment.js' }, - { from: 'moment/min/moment.min.js', to: 'moment.min.js' }, - ], - }, - - // Lodash - 'lodash': { - files: [ - { from: 'lodash/lodash.js', to: 'lodash.js' }, - { from: 'lodash/lodash.min.js', to: 'lodash.min.js' }, - ], - }, - - // Regenerator Runtime - 'regenerator-runtime': { - files: [ - { from: 'regenerator-runtime/runtime.js', to: 'regenerator-runtime.js' }, - { from: 'regenerator-runtime/runtime.js', to: 'regenerator-runtime.min.js' }, - ], - }, - - // React (UMD builds from node_modules) - 'react': { - files: [ - { from: 'react/umd/react.development.js', to: 'react.js' }, - { from: 'react/umd/react.production.min.js', to: 'react.min.js' }, - ], - }, - - // React DOM (UMD builds from node_modules) - 'react-dom': { - files: [ - { from: 'react-dom/umd/react-dom.development.js', to: 'react-dom.js' }, - { from: 'react-dom/umd/react-dom.production.min.js', to: 'react-dom.min.js' }, - ], - }, - - // Main Polyfill bundle - 'wp-polyfill': { - files: [ - { from: '@wordpress/babel-preset-default/build/polyfill.js', to: 'wp-polyfill.js' }, - { from: '@wordpress/babel-preset-default/build/polyfill.min.js', to: 'wp-polyfill.min.js' }, - ], - }, - - // Polyfills - Fetch (same source for both - was minified by webpack) - 'wp-polyfill-fetch': { - files: [ - { from: 'whatwg-fetch/dist/fetch.umd.js', to: 'wp-polyfill-fetch.js' }, - { from: 'whatwg-fetch/dist/fetch.umd.js', to: 'wp-polyfill-fetch.min.js' }, - ], - }, - - // Polyfills - FormData - 'wp-polyfill-formdata': { - files: [ - { from: 'formdata-polyfill/FormData.js', to: 'wp-polyfill-formdata.js' }, - { from: 'formdata-polyfill/formdata.min.js', to: 'wp-polyfill-formdata.min.js' }, - ], - }, - - // Polyfills - Element Closest (same for both) - 'wp-polyfill-element-closest': { - files: [ - { from: 'element-closest/browser.js', to: 'wp-polyfill-element-closest.js' }, - { from: 'element-closest/browser.js', to: 'wp-polyfill-element-closest.min.js' }, - ], - }, - - // Polyfills - Object Fit - 'wp-polyfill-object-fit': { - files: [ - { from: 'objectFitPolyfill/src/objectFitPolyfill.js', to: 'wp-polyfill-object-fit.js' }, - { from: 'objectFitPolyfill/dist/objectFitPolyfill.min.js', to: 'wp-polyfill-object-fit.min.js' }, - ], - }, - - // Polyfills - Inert - 'wp-polyfill-inert': { - files: [ - { from: 'wicg-inert/dist/inert.js', to: 'wp-polyfill-inert.js' }, - { from: 'wicg-inert/dist/inert.min.js', to: 'wp-polyfill-inert.min.js' }, - ], - }, - - // Polyfills - URL - 'wp-polyfill-url': { - files: [ - { from: 'core-js-url-browser/url.js', to: 'wp-polyfill-url.js' }, - { from: 'core-js-url-browser/url.min.js', to: 'wp-polyfill-url.min.js' }, - ], - }, - - // Polyfills - DOMRect (same source for both - was minified by webpack) - 'wp-polyfill-dom-rect': { - files: [ - { from: 'polyfill-library/polyfills/__dist/DOMRect/raw.js', to: 'wp-polyfill-dom-rect.js' }, - { from: 'polyfill-library/polyfills/__dist/DOMRect/raw.js', to: 'wp-polyfill-dom-rect.min.js' }, - ], - }, - - // Polyfills - Node.contains (same source for both - was minified by webpack) - 'wp-polyfill-node-contains': { - files: [ - { from: 'polyfill-library/polyfills/__dist/Node.prototype.contains/raw.js', to: 'wp-polyfill-node-contains.js' }, - { from: 'polyfill-library/polyfills/__dist/Node.prototype.contains/raw.js', to: 'wp-polyfill-node-contains.min.js' }, - ], - }, -}; - -/** - * Main execution function. - */ -async function main() { - console.log( '📦 Copying vendor scripts from node_modules...' ); - console.log( ` Build target: ${ buildTarget }/` ); - - // Create vendor directory - fs.mkdirSync( vendorDir, { recursive: true } ); - - let copied = 0; - let skipped = 0; - - for ( const [ vendor, config ] of Object.entries( VENDOR_FILES ) ) { - for ( const file of config.files ) { - const srcPath = path.join( nodeModulesDir, file.from ); - const destPath = path.join( vendorDir, file.to ); - - if ( fs.existsSync( srcPath ) ) { - fs.copyFileSync( srcPath, destPath ); - copied++; - } else { - console.log( ` ⚠️ Skipping ${ file.to }: source not found` ); - skipped++; - } - } - } - - console.log( `\n✅ Vendor scripts copied!` ); - console.log( ` Copied: ${ copied } files` ); - if ( skipped > 0 ) { - console.log( ` Skipped: ${ skipped } files` ); - } -} - -// Run main function -main().catch( ( error ) => { - console.error( '❌ Unexpected error:', error ); - process.exit( 1 ); -} ); From e9b3af76e86ce06f5b3a7ea4693307a97bc43c19 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Mon, 27 Apr 2026 13:56:21 +0000 Subject: [PATCH 31/57] Security: Update `composer/ca-bundle` to version `1.5.11`. This update adds 1 certificate to the bundle. Reviewed by desrosj, peterwilsoncc. Merges [62271] to the 7.0 branch. Props desrosj, peterwilsoncc. Fixes #64245. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62277 602fd350-edb4-49c9-b593-d223f7449a82 --- composer.json | 2 +- src/wp-includes/certificates/ca-bundle.crt | 28 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 17f53c2116f71..14479caefd5c1 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "ext-dom": "*" }, "require-dev": { - "composer/ca-bundle": "1.5.10", + "composer/ca-bundle": "1.5.11", "squizlabs/php_codesniffer": "3.13.5", "wp-coding-standards/wpcs": "~3.3.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", diff --git a/src/wp-includes/certificates/ca-bundle.crt b/src/wp-includes/certificates/ca-bundle.crt index 65be891eea878..a78e1dd471fa9 100644 --- a/src/wp-includes/certificates/ca-bundle.crt +++ b/src/wp-includes/certificates/ca-bundle.crt @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Dec 2 04:12:02 2025 GMT +## Certificate data from Mozilla last updated on: Wed Feb 11 18:26:30 2026 GMT ## ## Find updated versions here: https://curl.se/docs/caextract.html ## @@ -15,8 +15,8 @@ ## an Apache+mod_ssl webserver for SSL client authentication. ## Just configure this file as the SSLCACertificateFile. ## -## Conversion done with mk-ca-bundle.pl version 1.30. -## SHA256: a903b3cd05231e39332515ef7ebe37e697262f39515a52015c23c62805b73cd0 +## Conversion done with mk-ca-bundle.pl version 1.32. +## SHA256: 3b98d4e3ff57a326d9587c33633039c8c3a9cf0b55f7ca581d7598ff329eb1f3 ## @@ -3480,8 +3480,8 @@ SM49BAMDA2kAMGYCMQCpKjAd0MKfkFFRQD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxg ZzFDJe0CMQCSia7pXGKDYmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= -----END CERTIFICATE----- - OISTE Server Root RSA G1 -========================= +OISTE Server Root RSA G1 +======================== -----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBLMQswCQYDVQQG EwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJv @@ -3509,3 +3509,21 @@ msuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3J8tRd/iWkx7P8nd9H0aT olkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2wq1yVAb+axj5d9spLFKebXd7Yv0PTY6Y MjAwcRLWJTXjn/hvnLXrahut6hDTlhZyBiElxky8j3C7DOReIoMt0r7+hVu05L0= -----END CERTIFICATE----- + +e-Szigno TLS Root CA 2023 +========================= +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xFzAVBgNVBGEMDlZBVEhV +LTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBUTFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0 +MDAwMFoXDTM4MDcxNzE0MDAwMFowdTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYw +FAYDVQQKDA1NaWNyb3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZ +ZS1Temlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAGgP36J8 +PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFSAL/fjO1ZrTJlqwlZULUZ +wmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/vSzUaQ49CE0y5LBqcvjC2xN7cS53kpDzL +Ltmt3999Cd8ukv+ho2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUWYQCYlpGePVd3I8KECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0Uw +CgYIKoZIzj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpty7Ve +7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZlC9p2x1L/Cx6AcCIw +wzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6uWWL +-----END CERTIFICATE----- From aa24d91596105a8dba5df5ea68169bba4b4a88e9 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Tue, 28 Apr 2026 11:22:29 +0000 Subject: [PATCH 32/57] Revisions: Fix misplaced buttons in comparison UI. Align the Previous, Next, and Restore This Revision buttons consistently across viewports on the revisions comparison screen. Reviewed by peterwilsoncc. Merges [62273] to the 7.0 branch. Props audrasjb, mokshasharmila13, peterwilsoncc, presskopp, shailu25, wildworks. Fixes #65062. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62280 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/revisions.css | 5 ----- src/wp-admin/includes/revision.php | 8 ++++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/css/revisions.css b/src/wp-admin/css/revisions.css index 4cb824c8574c2..da238456178fc 100644 --- a/src/wp-admin/css/revisions.css +++ b/src/wp-admin/css/revisions.css @@ -309,7 +309,6 @@ table.diff .diff-addedline ins { float: right; margin-left: 6px; margin-right: 6px; - margin-top: 2px; } .diff-meta-from { @@ -632,8 +631,4 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { word-break: break-all; word-wrap: break-word; } - - .diff-meta input.restore-revision { - margin-top: 0; - } } diff --git a/src/wp-admin/includes/revision.php b/src/wp-admin/includes/revision.php index 979576ecde92c..2f15f1c9e2faf 100644 --- a/src/wp-admin/includes/revision.php +++ b/src/wp-admin/includes/revision.php @@ -370,11 +370,11 @@ function wp_print_revision_templates() { @@ -454,9 +454,9 @@ function wp_print_revision_templates() { <# } #> <# if ( data.attributes.autosave ) { #> - type="button" class="restore-revision button button-primary" value="" /> + type="button" class="restore-revision button button-primary button-compact" value="" /> <# } else { #> - type="button" class="restore-revision button button-primary" value="" /> + type="button" class="restore-revision button button-primary button-compact" value="" /> <# } #> <# } #> From 10307a760083cd2411b8456808a010536b49e4d6 Mon Sep 17 00:00:00 2001 From: Jb Audras Date: Wed, 29 Apr 2026 12:26:54 +0000 Subject: [PATCH 33/57] REST API: Harden Real Time Collaboration endpoint. Adds additional validation and permission checks the the Real Time Collaboration endpoint to ensure only input in the expected format is supported. Reviewed by peterwilsoncc, audrasjb. Merges [62198] to the 7.0 branch. Props czarate, westonruter, joefusco, peterwilsoncc. Fixes #64890. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62283 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-http-polling-sync-server.php | 102 +++++- .../tests/rest-api/rest-sync-server.php | 293 +++++++++++++++++- 2 files changed, 378 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index 88554a48c7d54..a90821ab78d3e 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -37,6 +37,30 @@ class WP_HTTP_Polling_Sync_Server { */ const COMPACTION_THRESHOLD = 50; + /** + * Maximum total size (in bytes) of the request body. + * + * @since 7.0.0 + * @var int + */ + const MAX_BODY_SIZE = 16 * MB_IN_BYTES; + + /** + * Maximum number of rooms allowed per request. + * + * @since 7.0.0 + * @var int + */ + const MAX_ROOMS_PER_REQUEST = 50; + + /** + * Maximum length of a single update data string. + * + * @since 7.0.0 + * @var int + */ + const MAX_UPDATE_DATA_SIZE = MB_IN_BYTES; + /** * Sync update type: compaction. * @@ -96,8 +120,9 @@ public function register_routes(): void { $typed_update_args = array( 'properties' => array( 'data' => array( - 'type' => 'string', - 'required' => true, + 'type' => 'string', + 'required' => true, + 'maxLength' => self::MAX_UPDATE_DATA_SIZE, ), 'type' => array( 'type' => 'string', @@ -149,12 +174,14 @@ public function register_routes(): void { 'methods' => array( WP_REST_Server::CREATABLE ), 'callback' => array( $this, 'handle_request' ), 'permission_callback' => array( $this, 'check_permissions' ), + 'validate_callback' => array( $this, 'validate_request' ), 'args' => array( 'rooms' => array( 'items' => array( 'properties' => $room_args, 'type' => 'object', ), + 'maxItems' => self::MAX_ROOMS_PER_REQUEST, 'required' => true, 'type' => 'array', ), @@ -223,6 +250,30 @@ public function check_permissions( WP_REST_Request $request ) { return true; } + /** + * Validates that the request body does not exceed the maximum allowed size. + * + * Runs as the route-level validate_callback, after per-arg schema + * validation has already passed. + * + * @since 7.0.0 + * + * @param WP_REST_Request $request The REST request. + * @return true|WP_Error True if valid, WP_Error if the body is too large. + */ + public function validate_request( WP_REST_Request $request ) { + $body = $request->get_body(); + if ( is_string( $body ) && strlen( $body ) > self::MAX_BODY_SIZE ) { + return new WP_Error( + 'rest_sync_body_too_large', + __( 'Request body is too large.' ), + array( 'status' => 413 ) + ); + } + + return true; + } + /** * Handles request: stores sync updates and awareness data, and returns * updates the client is missing. @@ -278,24 +329,47 @@ public function handle_request( WP_REST_Request $request ) { * * @param string $entity_kind The entity kind, e.g. 'postType', 'taxonomy', 'root'. * @param string $entity_name The entity name, e.g. 'post', 'category', 'site'. - * @param string|null $object_id The object ID / entity key for single entities, null for collections. + * @param string|null $object_id The numeric object ID / entity key for single entities, null for collections. * @return bool True if user has permission, otherwise false. */ private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool { - // Handle single post type entities with a defined object ID. - if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) { - return current_user_can( 'edit_post', (int) $object_id ); + if ( is_string( $object_id ) ) { + if ( ! ctype_digit( $object_id ) ) { + return false; + } + $object_id = (int) $object_id; } - - // Handle single taxonomy term entities with a defined object ID. - if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) { - $taxonomy = get_taxonomy( $entity_name ); - return isset( $taxonomy->cap->assign_terms ) && current_user_can( $taxonomy->cap->assign_terms ); + if ( null !== $object_id && $object_id <= 0 ) { + // Object ID must be numeric if provided. + return false; } - // Handle single comment entities with a defined object ID. - if ( 'root' === $entity_kind && 'comment' === $entity_name && is_numeric( $object_id ) ) { - return current_user_can( 'edit_comment', (int) $object_id ); + // Validate permissions for the provided object ID. + if ( is_int( $object_id ) ) { + // Handle single post type entities with a defined object ID. + if ( 'postType' === $entity_kind ) { + if ( get_post_type( $object_id ) !== $entity_name ) { + // Post is not of the specified post type. + return false; + } + return current_user_can( 'edit_post', $object_id ); + } + + // Handle single taxonomy term entities with a defined object ID. + if ( 'taxonomy' === $entity_kind ) { + $term_exists = term_exists( $object_id, $entity_name ); + if ( ! is_array( $term_exists ) || ! isset( $term_exists['term_id'] ) ) { + // Either term doesn't exist OR term is not in specified taxonomy. + return false; + } + + return current_user_can( 'edit_term', $object_id ); + } + + // Handle single comment entities with a defined object ID. + if ( 'root' === $entity_kind && 'comment' === $entity_name ) { + return current_user_can( 'edit_comment', $object_id ); + } } // All the remaining checks are for collections. If an object ID is provided, diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index 7a04226ced8c9..7ded16bd3b033 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -9,14 +9,20 @@ */ class WP_Test_REST_Sync_Server extends WP_Test_REST_Controller_Testcase { - protected static $editor_id; - protected static $subscriber_id; - protected static $post_id; + protected static int $editor_id; + protected static int $subscriber_id; + protected static int $post_id; + protected static int $category_id; + protected static int $tag_id; + protected static int $comment_id; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); self::$post_id = $factory->post->create( array( 'post_author' => self::$editor_id ) ); + self::$category_id = $factory->category->create(); + self::$tag_id = $factory->tag->create(); + self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) ); // Enable option in setUpBeforeClass to ensure REST routes are registered. update_option( 'wp_collaboration_enabled', 1 ); @@ -27,6 +33,9 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$subscriber_id ); delete_option( 'wp_collaboration_enabled' ); wp_delete_post( self::$post_id, true ); + wp_delete_term( self::$category_id, 'category' ); + wp_delete_term( self::$tag_id, 'post_tag' ); + wp_delete_comment( self::$comment_id, true ); } public function set_up() { @@ -277,6 +286,107 @@ public function test_sync_permission_checked_per_room() { $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); } + /** + * @ticket 64890 + */ + public function test_sync_malformed_object_id_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:1abc' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_zero_object_id_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:0' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_post_type_mismatch_rejected(): void { + wp_set_current_user( self::$editor_id ); + + // The test post is of type 'post', not 'page'. + $response = $this->dispatch_sync( array( $this->build_room( 'postType/page:' . self::$post_id ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_taxonomy_term_allowed(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$category_id ) ) ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_taxonomy_term_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:999999' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_taxonomy_term_wrong_taxonomy_rejected(): void { + wp_set_current_user( self::$editor_id ); + + // The tag term exists in 'post_tag', not 'category'. + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$tag_id ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_comment_allowed(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:' . self::$comment_id ) ) ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_comment_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:999999' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_post_type_collection_rejected(): void { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/nonexistent_type' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + /* * Validation tests. */ @@ -293,6 +403,183 @@ public function test_sync_invalid_room_format_rejected() { $this->assertSame( 400, $response->get_status() ); } + /** + * Verifies that schema type validation rejects a non-string value for the + * update 'data' field, confirming that per-arg schema validation still runs + * with a route-level validate_callback registered. + * + * @ticket 64890 + */ + public function test_sync_rejects_non_string_update_data(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => 12345, + 'type' => 'update', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that schema enum validation rejects an invalid update type, + * confirming that per-arg schema validation still runs with a route-level + * validate_callback registered. + * + * @ticket 64890 + */ + public function test_sync_rejects_invalid_update_type_enum(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => 'dGVzdA==', + 'type' => 'invalid_type', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that schema required-field validation rejects a room missing + * the 'client_id' field, confirming that per-arg schema validation still + * runs with a route-level validate_callback registered. + * + * @ticket 64890 + */ + public function test_sync_rejects_missing_required_room_field(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + // 'client_id' deliberately omitted. + 'room' => $this->get_post_room(), + 'updates' => array(), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the maxItems constraint rejects a request with more rooms + * than MAX_ROOMS_PER_REQUEST. + * + * @ticket 64890 + */ + public function test_sync_rejects_rooms_exceeding_max_items(): void { + wp_set_current_user( self::$editor_id ); + + $rooms = array(); + for ( $i = 0; $i < WP_HTTP_Polling_Sync_Server::MAX_ROOMS_PER_REQUEST + 1; $i++ ) { + $rooms[] = $this->build_room( 'root/site', $i + 1 ); + } + + $response = $this->dispatch_sync( $rooms ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the maxLength constraint rejects update data exceeding + * MAX_UPDATE_DATA_SIZE. + * + * @ticket 64890 + */ + public function test_sync_rejects_update_data_exceeding_max_length(): void { + wp_set_current_user( self::$editor_id ); + + $oversized_data = str_repeat( 'a', WP_HTTP_Polling_Sync_Server::MAX_UPDATE_DATA_SIZE + 1 ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => $oversized_data, + 'type' => 'update', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the route-level validate_callback rejects a request body + * exceeding MAX_BODY_SIZE. + * + * @ticket 64890 + */ + public function test_sync_rejects_oversized_request_body(): void { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + + // Set valid parsed params so per-arg schema validation passes first. + $request->set_body_params( + array( + 'rooms' => array( + $this->build_room( $this->get_post_room() ), + ), + ) + ); + + // Set an oversized raw body to trigger the route-level validate_callback. + $request->set_body( str_repeat( 'x', WP_HTTP_Polling_Sync_Server::MAX_BODY_SIZE + 1 ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_sync_body_too_large', $response, 413 ); + } + /* * Response format tests. */ From ee1b3fd3bf8c3762c3839a0c6f38b83c2d1b7323 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 30 Apr 2026 04:53:57 +0000 Subject: [PATCH 34/57] Administration: Correct alignment of meta boxes on the Edit screen with classic editor. Follow-up to [61646], [61759]. Reviewed by peterwilsoncc. Merges r62284 to the 7.0 branch. Props umeshnevase, sabernhardt, SergeyBiryukov. Fixes #65141. git-svn-id: https://develop.svn.wordpress.org/branches/7.0@62285 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/dashboard.css | 1 - src/wp-admin/css/edit.css | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index 324637a7a7b08..ab73f828f7067 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -60,7 +60,6 @@ /* Required min-height to make the jQuery UI Sortable drop zone work. */ min-height: 0; margin: 0 8px 20px; - padding: 0; } #dashboard-widgets .meta-box-sortables:not(:empty) { diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index 133616d335a6d..aa0ecc69943cb 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -173,10 +173,6 @@ body.post-type-wp_navigation .inline-edit-status { /* Post Screen */ -.metabox-holder .postbox-container .meta-box-sortables { - padding: 4px; -} - /* Only highlight drop zones when dragging and only in the 2 columns layout. */ .is-dragging-metaboxes .metabox-holder .postbox-container .meta-box-sortables { border-radius: 8px; From d592047c4705a445a5a8cf30365daa55e63a5546 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 30 Apr 2026 04:58:46 +0000 Subject: [PATCH 35/57] I18N: Add translation support for script modules. Add automatic translation loading for script modules (ES modules), so strings using `__()` and friends from `@wordpress/i18n` can be translated at runtime. This brings classic script i18n parity to script modules registered via `wp_register_script_module()`, which previously had no way to load translation data, leaving strings untranslated on screens like Connectors and Fonts that are built as script modules. At the `admin_print_footer_scripts` and `wp_footer` actions, every enqueued script module and its dependencies are walked, the translation chunk is loaded for each, and an inline `