diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 3bb7a60d1d233..88b006b237b06 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -12,26 +12,26 @@ on: - '[0-9]+.[0-9]' - '[0-9]+.[0-9].[0-9]+' - '!3.7.[0-9]+' - pull_request: - branches: - - trunk - - '3.[89]' - - '[4-9].[0-9]' - paths: - # Any change to a PHP or JavaScript file should run checks. - - '**.js' - - '**.php' - # These files configure npm. Changes could affect the outcome. - - 'package*.json' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This file configures JSHint. Changes could affect the outcome. - - '.jshintrc' - # This file configures PHPCS. Changes could affect the outcome. - - 'phpcs.xml.dist' - # Confirm any changes to relevant workflow files. - - '.github/workflows/coding-standards.yml' - - '.github/workflows/reusable-coding-standards-*.yml' + # pull_request: + # branches: + # - trunk + # - '3.[89]' + # - '[4-9].[0-9]' + # paths: + # # Any change to a PHP or JavaScript file should run checks. + # - '**.js' + # - '**.php' + # # These files configure npm. Changes could affect the outcome. + # - 'package*.json' + # # These files configure Composer. Changes could affect the outcome. + # - 'composer.*' + # # This file configures JSHint. Changes could affect the outcome. + # - '.jshintrc' + # # This file configures PHPCS. Changes could affect the outcome. + # - 'phpcs.xml.dist' + # # Confirm any changes to relevant workflow files. + # - '.github/workflows/coding-standards.yml' + # - '.github/workflows/reusable-coding-standards-*.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index cc8f34810106c..412d11c1318ef 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -12,28 +12,28 @@ on: - '[0-9]+.[0-9].[0-9]+' - '![34].[0-9].[0-9]+' - '!5.[0-2].[0-9]+' - pull_request: - branches: - - trunk - - '5.[3-9]' - - '[6-9].[0-9]' - paths: - # Any change to a PHP, CSS, or JavaScript file should run checks. - - '**.css' - - '**.js' - - '**.php' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - 'Gruntfile.js' - - 'webpack.config.js' - - 'tools/webpack/**' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This files affect the e2e tests. Changes could affect the outcome. - - 'tests/e2e/**' - # Confirm any changes to relevant workflow files. - - '.github/workflows/end-to-end-tests.yml' - - '.github/workflows/reusable-end-to-end-tests-*.yml' + # pull_request: + # branches: + # - trunk + # - '5.[3-9]' + # - '[6-9].[0-9]' + # paths: + # # Any change to a PHP, CSS, or JavaScript file should run checks. + # - '**.css' + # - '**.js' + # - '**.php' + # # These files configure npm and the task runner. Changes could affect the outcome. + # - 'package*.json' + # - 'Gruntfile.js' + # - 'webpack.config.js' + # - 'tools/webpack/**' + # # These files configure Composer. Changes could affect the outcome. + # - 'composer.*' + # # This files affect the e2e tests. Changes could affect the outcome. + # - 'tests/e2e/**' + # # Confirm any changes to relevant workflow files. + # - '.github/workflows/end-to-end-tests.yml' + # - '.github/workflows/reusable-end-to-end-tests-*.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 27339c1eefc41..3d56739265753 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -11,29 +11,29 @@ on: - '[0-9]+.[0-9].[0-9]+' - '![45].[0-9].[0-9]+' - '!6.[01].[0-9]+' - pull_request: - branches: - - trunk - - '6.[2-9]' - - '[7-9].[0-9]' - paths: - # Any change to a PHP, CSS, or JavaScript file should run checks. - - '**.css' - - '**.js' - - '**.php' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - 'Gruntfile.js' - - 'webpack.config.js' - - 'tools/webpack/**' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This files affect the performance tests. Changes could affect the outcome. - - 'tests/performance/**' - # Confirm any changes to relevant workflow files. - - '.github/workflows/performance.yml' - - '.github/workflows/reusable-performance.yml' - - '.github/workflows/reusable-performance-*.yml' + # pull_request: + # branches: + # - trunk + # - '6.[2-9]' + # - '[7-9].[0-9]' + # paths: + # # Any change to a PHP, CSS, or JavaScript file should run checks. + # - '**.css' + # - '**.js' + # - '**.php' + # # These files configure npm and the task runner. Changes could affect the outcome. + # - 'package*.json' + # - 'Gruntfile.js' + # - 'webpack.config.js' + # - 'tools/webpack/**' + # # These files configure Composer. Changes could affect the outcome. + # - 'composer.*' + # # This files affect the performance tests. Changes could affect the outcome. + # - 'tests/performance/**' + # # Confirm any changes to relevant workflow files. + # - '.github/workflows/performance.yml' + # - '.github/workflows/reusable-performance.yml' + # - '.github/workflows/reusable-performance-*.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index c712e50476dbe..1a8d1406f71a6 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -12,21 +12,21 @@ on: - '[0-9]+.[0-9].[0-9]+' - '![34].[0-9].[0-9]+' - '!5.[0-4].[0-9]+' - pull_request: - branches: - - trunk - - '5.[5-9]' - - '[6-9].[0-9]' - paths: - # This workflow only scans PHP files. - - '**.php' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This file configures PHP compatibility scanning. Changes could affect the outcome. - - 'phpcompat.xml.dist' - # Confirm any changes to relevant workflow files. - - '.github/workflows/php-compatibility.yml' - - '.github/workflows/reusable-php-compatibility.yml' + # pull_request: + # branches: + # - trunk + # - '5.[5-9]' + # - '[6-9].[0-9]' + # paths: + # # This workflow only scans PHP files. + # - '**.php' + # # These files configure Composer. Changes could affect the outcome. + # - 'composer.*' + # # This file configures PHP compatibility scanning. Changes could affect the outcome. + # - 'phpcompat.xml.dist' + # # Confirm any changes to relevant workflow files. + # - '.github/workflows/php-compatibility.yml' + # - '.github/workflows/reusable-php-compatibility.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 44c1476980fde..87cd059358459 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -9,30 +9,30 @@ on: tags: - '[0-9]+.[0-9]' - '[0-9]+.[0-9].[0-9]+' - pull_request: - branches: - - trunk - - '3.[7-9]' - - '[4-9].[0-9]' - paths: - # Any change to a PHP, CSS, JavaScript, JSON, HTML, or otherwise tested file should run checks. - - '**.css' - - '**.html' - - '**.js' - - '**.json' - - '**.php' - - 'src/license.txt' - - 'src/SECURITY.md' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - 'Gruntfile.js' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # This files affect the phpunit tests. Changes could affect the outcome. - - 'tests/phpunit/**' - # Confirm any changes to relevant workflow files. - - '.github/workflows/phpunit-tests.yml' - - '.github/workflows/reusable-phpunit-tests-*.yml' + # pull_request: + # branches: + # - trunk + # - '3.[7-9]' + # - '[4-9].[0-9]' + # paths: + # # Any change to a PHP, CSS, JavaScript, JSON, HTML, or otherwise tested file should run checks. + # - '**.css' + # - '**.html' + # - '**.js' + # - '**.json' + # - '**.php' + # - 'src/license.txt' + # - 'src/SECURITY.md' + # # These files configure npm and the task runner. Changes could affect the outcome. + # - 'package*.json' + # - 'Gruntfile.js' + # # These files configure Composer. Changes could affect the outcome. + # - 'composer.*' + # # This files affect the phpunit tests. Changes could affect the outcome. + # - 'tests/phpunit/**' + # # Confirm any changes to relevant workflow files. + # - '.github/workflows/phpunit-tests.yml' + # - '.github/workflows/reusable-phpunit-tests-*.yml' workflow_dispatch: # Once weekly On Sundays at 00:00 UTC. schedule: diff --git a/.github/workflows/reusable-build-package.yml b/.github/workflows/reusable-build-package.yml new file mode 100644 index 0000000000000..f7c744af7c34a --- /dev/null +++ b/.github/workflows/reusable-build-package.yml @@ -0,0 +1,60 @@ +## +# 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.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@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: wordpress-develop + path: develop.zip + if-no-files-found: error diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml index 1c617ee91f36f..22c68924c2456 100644 --- a/.github/workflows/reusable-upgrade-testing.yml +++ b/.github/workflows/reusable-upgrade-testing.yml @@ -14,7 +14,7 @@ on: required: true type: 'string' new-version: - description: 'The version of WordPress to update to.' + 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: @@ -36,6 +36,11 @@ on: required: false type: 'string' default: '5.7' + extensions: + description: 'PHP extension configuration used by setup-php.' + required: false + type: 'string' + default: '' # Disable permissions for all available scopes by default. # Any needed permissions should be configured at the job level. @@ -49,8 +54,13 @@ jobs: # - 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: ${{ inputs.wp }} to ${{ inputs.new-version }} / PHP ${{ inputs.php }} with ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }} permissions: {} @@ -79,6 +89,16 @@ jobs: php-version: '${{ inputs.php }}' coverage: none tools: wp-cli + extensions: ${{ inputs.extensions }} + env: + fail-fast: true + + - name: Debug information + run: | + php --version + php -m + php -i + locale -a - name: Download WordPress ${{ inputs.wp }} run: wp core download --version="${WP_VERSION}" @@ -96,11 +116,39 @@ jobs: --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@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + 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 + if: always() + run: wp core version diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml index 2170bc689b8c3..139aebc073ae8 100644 --- a/.github/workflows/test-build-processes.yml +++ b/.github/workflows/test-build-processes.yml @@ -9,28 +9,28 @@ on: tags: - '[0-9]+.[0-9]' - '[0-9]+.[0-9].[0-9]+' - pull_request: - branches: - - trunk - - '3.[7-9]' - - '[4-9].[0-9]' - paths: - # Any change to a PHP, CSS, JavaScript, or JSON file should run checks. - - '**.css' - - '**.js' - - '**.json' - - '**.php' - # These files configure npm and the task runner. Changes could affect the outcome. - - 'package*.json' - - 'Gruntfile.js' - - 'webpack.config.js' - - 'tools/webpack/**' - # These files configure Composer. Changes could affect the outcome. - - 'composer.*' - # Confirm any changes to relevant workflow files. - - '.github/workflows/test-build-processes.yml' - - '.github/workflows/reusable-test-core-build-process.yml' - - '.github/workflows/reusable-test-gutenberg-build-process.yml' + # pull_request: + # branches: + # - trunk + # - '3.[7-9]' + # - '[4-9].[0-9]' + # paths: + # # Any change to a PHP, CSS, JavaScript, or JSON file should run checks. + # - '**.css' + # - '**.js' + # - '**.json' + # - '**.php' + # # These files configure npm and the task runner. Changes could affect the outcome. + # - 'package*.json' + # - 'Gruntfile.js' + # - 'webpack.config.js' + # - 'tools/webpack/**' + # # These files configure Composer. Changes could affect the outcome. + # - 'composer.*' + # # Confirm any changes to relevant workflow files. + # - '.github/workflows/test-build-processes.yml' + # - '.github/workflows/reusable-test-core-build-process.yml' + # - '.github/workflows/reusable-test-gutenberg-build-process.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. diff --git a/.github/workflows/upgrade-develop-testing.yml b/.github/workflows/upgrade-develop-testing.yml new file mode 100644 index 0000000000000..804eb3259b798 --- /dev/null +++ b/.github/workflows/upgrade-develop-testing.yml @@ -0,0 +1,139 @@ +# Confirms that updating WordPress using WP-CLI works successfully. +# +# This workflow tests upgrading from a previous version to the current wordpress-develop checkout, not to a version available on WordPress.org. +name: Upgrade Develop Version Tests + +on: + push: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + tags: + - '[0-9]+.[0-9]' + - '[0-9]+.[0-9].[0-9]+' + paths: + # Any change to a source PHP file should run checks. + - 'src/**.php' + # Confirm any changes to relevant workflow files. + - '.github/workflows/upgrade-develop-testing.yml' + - '.github/workflows/reusable-upgrade-testing.yml' + pull_request: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + paths: + # Any change to a source PHP file should run checks. + - 'src/**.php' + # Confirm any changes to relevant workflow files. + - '.github/workflows/upgrade-develop-testing.yml' + - '.github/workflows/reusable-upgrade-testing.yml' + 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: {} + +jobs: + # Build WordPress from the current branch ready for the upgrade tests. + build: + name: Build + uses: ./.github/workflows/reusable-build-package.yml + permissions: + contents: read + + # Run upgrade tests for the current branch. + upgrade-tests-develop: + name: Upgrade from ${{ matrix.wp }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + needs: [ build ] + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.2', '8.4' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.4' ] + wp: [ '6.5', '6.6', '6.7' ] + multisite: [ false, true ] + extensions: [ '' ] + + include: + - os: 'ubuntu-24.04' + php: '7.2' + db-type: 'mysql' + db-version: '5.7' + wp: '6.5' + multisite: false + extensions: 'none, mysqli, json, curl, zip, iconv' + exclude: + # The PHP <= 7.3/MySQL 8.4 jobs currently fail due to mysql_native_password being disabled by default. See https://core.trac.wordpress.org/ticket/61218. + - php: '7.2' + db-version: '8.4' + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: develop + multisite: ${{ matrix.multisite }} + extensions: ${{ matrix.extensions }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ upgrade-tests-develop ] + 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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + 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/upgrade-testing.yml b/.github/workflows/upgrade-testing.yml index a8b4a9b1c2a46..965adc616d424 100644 --- a/.github/workflows/upgrade-testing.yml +++ b/.github/workflows/upgrade-testing.yml @@ -11,11 +11,11 @@ on: paths: - '.github/workflows/upgrade-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' - pull_request: - # Always test the workflow when changes are suggested. - paths: - - '.github/workflows/upgrade-testing.yml' - - '.github/workflows/reusable-upgrade-testing.yml' + # pull_request: + # # 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: diff --git a/composer.json b/composer.json index aea6ea7a42648..0a8a9c78094c7 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "issues": "https://core.trac.wordpress.org/" }, "require": { + "ext-hash": "*", "ext-json": "*", "php": ">=7.2.24" }, diff --git a/src/index.php b/src/index.php index 91c0517857339..2d56e43978b6a 100644 --- a/src/index.php +++ b/src/index.php @@ -1,5 +1,7 @@ array( 'function' => 'hash', - 'required' => false, + 'required' => true, ), 'imagick' => array( 'extension' => 'imagick', diff --git a/src/wp-admin/includes/update-core.php b/src/wp-admin/includes/update-core.php index a29f389860127..941e5e1b25b0d 100644 --- a/src/wp-admin/includes/update-core.php +++ b/src/wp-admin/includes/update-core.php @@ -1009,9 +1009,6 @@ * @global array $_old_requests_files * @global array $_new_bundled_files * @global wpdb $wpdb WordPress database abstraction object. - * @global string $wp_version - * @global string $required_php_version - * @global string $required_mysql_version * * @param string $from New release unzipped path. * @param string $to Path to old WordPress installation. @@ -1075,7 +1072,7 @@ function update_core( $from, $to ) { } /* - * Import $wp_version, $required_php_version, and $required_mysql_version from the new version. + * Import $wp_version, $required_php_version, $required_php_extensions, and $required_mysql_version from the new version. * DO NOT globalize any variables imported from `version-current.php` in this function. * * BC Note: $wp_filesystem->wp_content_dir() returned unslashed pre-2.8. @@ -1181,17 +1178,29 @@ function update_core( $from, $to ) { ); } - // Add a warning when the JSON PHP extension is missing. - if ( ! extension_loaded( 'json' ) ) { - return new WP_Error( - 'php_not_compatible_json', - sprintf( - /* translators: 1: WordPress version number, 2: The PHP extension name needed. */ - __( 'The update cannot be installed because WordPress %1$s requires the %2$s PHP extension.' ), - $wp_version, - 'JSON' - ) - ); + if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) { + $missing_extensions = new WP_Error(); + + foreach ( $required_php_extensions as $extension ) { + if ( extension_loaded( $extension ) ) { + continue; + } + + $missing_extensions->add( + "php_not_compatible_{$extension}", + sprintf( + /* translators: 1: WordPress version number, 2: The PHP extension name needed. */ + __( 'The update cannot be installed because WordPress %1$s requires the %2$s PHP extension.' ), + $wp_version, + $extension + ) + ); + } + + // Add a warning when required PHP extensions are missing. + if ( $missing_extensions->has_errors() ) { + return $missing_extensions; + } } /** This filter is documented in wp-admin/includes/update-core.php */ diff --git a/src/wp-admin/install.php b/src/wp-admin/install.php index e81331acfc82e..736c5138e96ec 100644 --- a/src/wp-admin/install.php +++ b/src/wp-admin/install.php @@ -232,12 +232,13 @@ function display_setup_form( $error = null ) { } /** - * @global string $wp_version The WordPress version string. - * @global string $required_php_version The required PHP version string. - * @global string $required_mysql_version The required MySQL version string. - * @global wpdb $wpdb WordPress database abstraction object. + * @global string $wp_version The WordPress version string. + * @global string $required_php_version The required PHP version string. + * @global string[] $required_php_extensions The names of required PHP extensions. + * @global string $required_mysql_version The required MySQL version string. + * @global wpdb $wpdb WordPress database abstraction object. */ -global $wp_version, $required_php_version, $required_mysql_version, $wpdb; +global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb; $php_version = PHP_VERSION; $mysql_version = $wpdb->db_version(); @@ -298,6 +299,29 @@ function display_setup_form( $error = null ) { die( '

' . __( 'Requirements Not Met' ) . '

' . $compat . '

' ); } +if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) { + $missing_extensions = array(); + + foreach ( $required_php_extensions as $extension ) { + if ( extension_loaded( $extension ) ) { + continue; + } + + $missing_extensions[] = sprintf( + /* translators: 1: URL to WordPress release notes, 2: WordPress version number, 3: The PHP extension name needed. */ + __( 'You cannot install because WordPress %2$s requires the %3$s PHP extension.' ), + $version_url, + $wp_version, + $extension + ); + } + + if ( count( $missing_extensions ) > 0 ) { + display_header(); + die( '

' . __( 'Requirements Not Met' ) . '

' . implode( '

', $missing_extensions ) . '

' ); + } +} + if ( ! is_string( $wpdb->base_prefix ) || '' === $wpdb->base_prefix ) { display_header(); die( diff --git a/src/wp-admin/upgrade.php b/src/wp-admin/upgrade.php index 9edadd9be2f1c..ea98082704070 100644 --- a/src/wp-admin/upgrade.php +++ b/src/wp-admin/upgrade.php @@ -36,12 +36,13 @@ } /** - * @global string $wp_version The WordPress version string. - * @global string $required_php_version The required PHP version string. - * @global string $required_mysql_version The required MySQL version string. - * @global wpdb $wpdb WordPress database abstraction object. + * @global string $wp_version The WordPress version string. + * @global string $required_php_version The required PHP version string. + * @global string[] $required_php_extensions The names of required PHP extensions. + * @global string $required_mysql_version The required MySQL version string. + * @global wpdb $wpdb WordPress database abstraction object. */ -global $wp_version, $required_php_version, $required_mysql_version, $wpdb; +global $wp_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wpdb; $step = (int) $step; @@ -54,6 +55,24 @@ $mysql_compat = version_compare( $mysql_version, $required_mysql_version, '>=' ); } +$missing_extensions = array(); + +if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) { + foreach ( $required_php_extensions as $extension ) { + if ( extension_loaded( $extension ) ) { + continue; + } + + $missing_extensions[] = sprintf( + /* translators: 1: URL to WordPress release notes, 2: WordPress version number, 3: The PHP extension name needed. */ + __( 'You cannot upgrade because WordPress %2$s requires the %3$s PHP extension.' ), + $version_url, + $wp_version, + $extension + ); + } +} + header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); ?> @@ -126,8 +145,8 @@ } echo '

' . $message . '

'; - ?> - 0 ) : + echo '

' . implode( '

', $missing_extensions ) . '

'; else : switch ( $step ) : case 0: diff --git a/src/wp-includes/class-wp-session-tokens.php b/src/wp-includes/class-wp-session-tokens.php index feabf698b7467..9482e1b948777 100644 --- a/src/wp-includes/class-wp-session-tokens.php +++ b/src/wp-includes/class-wp-session-tokens.php @@ -68,12 +68,7 @@ final public static function get_instance( $user_id ) { * @return string A hash of the session token (a verifier). */ private function hash_token( $token ) { - // If ext/hash is not present, use sha1() instead. - if ( function_exists( 'hash' ) ) { - return hash( 'sha256', $token ); - } else { - return sha1( $token ); - } + return hash( 'sha256', $token ); } /** diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php index 5ace90d490e17..c6e6099c260cd 100644 --- a/src/wp-includes/class-wpdb.php +++ b/src/wp-includes/class-wpdb.php @@ -2412,12 +2412,10 @@ public function placeholder_escape() { static $placeholder; if ( ! $placeholder ) { - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; // Old WP installs may not have AUTH_SALT defined. $salt = defined( 'AUTH_SALT' ) && AUTH_SALT ? AUTH_SALT : (string) rand(); - $placeholder = '{' . hash_hmac( $algo, uniqid( $salt, true ), $salt ) . '}'; + $placeholder = '{' . hash_hmac( 'sha256', uniqid( $salt, true ), $salt ) . '}'; } /* diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php index 6a393f7c984e3..b728e6bc77dea 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -263,118 +263,6 @@ function _mb_strlen( $str, $encoding = null ) { return --$count; } -if ( ! function_exists( 'hash_hmac' ) ) : - /** - * Compat function to mimic hash_hmac(). - * - * The Hash extension is bundled with PHP by default since PHP 5.1.2. - * However, the extension may be explicitly disabled on select servers. - * As of PHP 7.4.0, the Hash extension is a core PHP extension and can no - * longer be disabled. - * I.e. when PHP 7.4.0 becomes the minimum requirement, this polyfill - * and the associated `_hash_hmac()` function can be safely removed. - * - * @ignore - * @since 3.2.0 - * - * @see _hash_hmac() - * - * @param string $algo Hash algorithm. Accepts 'md5' or 'sha1'. - * @param string $data Data to be hashed. - * @param string $key Secret key to use for generating the hash. - * @param bool $binary Optional. Whether to output raw binary data (true), - * or lowercase hexits (false). Default false. - * @return string|false The hash in output determined by `$binary`. - * False if `$algo` is unknown or invalid. - */ - function hash_hmac( $algo, $data, $key, $binary = false ) { - return _hash_hmac( $algo, $data, $key, $binary ); - } -endif; - -/** - * Internal compat function to mimic hash_hmac(). - * - * @ignore - * @since 3.2.0 - * - * @param string $algo Hash algorithm. Accepts 'md5' or 'sha1'. - * @param string $data Data to be hashed. - * @param string $key Secret key to use for generating the hash. - * @param bool $binary Optional. Whether to output raw binary data (true), - * or lowercase hexits (false). Default false. - * @return string|false The hash in output determined by `$binary`. - * False if `$algo` is unknown or invalid. - */ -function _hash_hmac( $algo, $data, $key, $binary = false ) { - $packs = array( - 'md5' => 'H32', - 'sha1' => 'H40', - ); - - if ( ! isset( $packs[ $algo ] ) ) { - return false; - } - - $pack = $packs[ $algo ]; - - if ( strlen( $key ) > 64 ) { - $key = pack( $pack, $algo( $key ) ); - } - - $key = str_pad( $key, 64, chr( 0 ) ); - - $ipad = ( substr( $key, 0, 64 ) ^ str_repeat( chr( 0x36 ), 64 ) ); - $opad = ( substr( $key, 0, 64 ) ^ str_repeat( chr( 0x5C ), 64 ) ); - - $hmac = $algo( $opad . pack( $pack, $algo( $ipad . $data ) ) ); - - if ( $binary ) { - return pack( $pack, $hmac ); - } - - return $hmac; -} - -if ( ! function_exists( 'hash_equals' ) ) : - /** - * Timing attack safe string comparison. - * - * Compares two strings using the same time whether they're equal or not. - * - * Note: It can leak the length of a string when arguments of differing length are supplied. - * - * This function was added in PHP 5.6. - * However, the Hash extension may be explicitly disabled on select servers. - * As of PHP 7.4.0, the Hash extension is a core PHP extension and can no - * longer be disabled. - * I.e. when PHP 7.4.0 becomes the minimum requirement, this polyfill - * can be safely removed. - * - * @since 3.9.2 - * - * @param string $known_string Expected string. - * @param string $user_string Actual, user supplied, string. - * @return bool Whether strings are equal. - */ - function hash_equals( $known_string, $user_string ) { - $known_string_length = strlen( $known_string ); - - if ( strlen( $user_string ) !== $known_string_length ) { - return false; - } - - $result = 0; - - // Do not attempt to "optimize" this. - for ( $i = 0; $i < $known_string_length; $i++ ) { - $result |= ord( $known_string[ $i ] ) ^ ord( $user_string[ $i ] ); - } - - return 0 === $result; - } -endif; - // sodium_crypto_box() was introduced in PHP 7.2. if ( ! function_exists( 'sodium_crypto_box' ) ) { require ABSPATH . WPINC . '/sodium_compat/autoload.php'; diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 5066cd16ee341..0526cb175dfb6 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -147,11 +147,12 @@ function wp_populate_basic_auth_from_authorization_header() { * @since 3.0.0 * @access private * - * @global string $required_php_version The required PHP version string. - * @global string $wp_version The WordPress version string. + * @global string $required_php_version The required PHP version string. + * @global string[] $required_php_extensions The names of required PHP extensions. + * @global string $wp_version The WordPress version string. */ function wp_check_php_mysql_versions() { - global $required_php_version, $wp_version; + global $required_php_version, $required_php_extensions, $wp_version; $php_version = PHP_VERSION; @@ -168,6 +169,30 @@ function wp_check_php_mysql_versions() { exit( 1 ); } + $missing_extensions = array(); + + if ( isset( $required_php_extensions ) && is_array( $required_php_extensions ) ) { + foreach ( $required_php_extensions as $extension ) { + if ( extension_loaded( $extension ) ) { + continue; + } + + $missing_extensions[] = sprintf( + 'WordPress %1$s requires the %2$s PHP extension.', + $wp_version, + $extension + ); + } + } + + if ( count( $missing_extensions ) > 0 ) { + $protocol = wp_get_server_protocol(); + header( sprintf( '%s 500 Internal Server Error', $protocol ), true, 500 ); + header( 'Content-Type: text/html; charset=utf-8' ); + echo implode( '
', $missing_extensions ); + exit( 1 ); + } + // This runs before default constants are defined, so we can't assume WP_CONTENT_DIR is set yet. $wp_content_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : ABSPATH . 'wp-content'; diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 60ad0841ed8ce..bc61ba0bdfa3b 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -772,9 +772,7 @@ function wp_validate_auth_cookie( $cookie = '', $scheme = '' ) { $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; - $hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, $key ); + $hash = hash_hmac( 'sha256', $username . '|' . $expiration . '|' . $token, $key ); if ( ! hash_equals( $hash, $hmac ) ) { /** @@ -875,9 +873,7 @@ function wp_generate_auth_cookie( $user_id, $expiration, $scheme = 'auth', $toke $key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme ); - // If ext/hash is not present, compat.php's hash_hmac() does not support sha256. - $algo = function_exists( 'hash' ) ? 'sha256' : 'sha1'; - $hash = hash_hmac( $algo, $user->user_login . '|' . $expiration . '|' . $token, $key ); + $hash = hash_hmac( 'sha256', $user->user_login . '|' . $expiration . '|' . $token, $key ); $cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash; diff --git a/src/wp-includes/version.php b/src/wp-includes/version.php index ce3923701fd70..241d90316c075 100644 --- a/src/wp-includes/version.php +++ b/src/wp-includes/version.php @@ -39,6 +39,17 @@ */ $required_php_version = '7.2.24'; +/** + * Holds the names of required PHP extensions. + * + * @global string[] $required_php_extensions + */ +$required_php_extensions = array( + 'json', + 'hash', + 'pineapple', +); + /** * Holds the required MySQL version. * diff --git a/src/wp-settings.php b/src/wp-settings.php index ec08ff11498f3..05a13f21a7166 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -22,14 +22,15 @@ * include version.php from another installation and don't override * these values if already set. * - * @global string $wp_version The WordPress version string. - * @global int $wp_db_version WordPress database version. - * @global string $tinymce_version TinyMCE version. - * @global string $required_php_version The required PHP version string. - * @global string $required_mysql_version The required MySQL version string. - * @global string $wp_local_package Locale code of the package. + * @global string $wp_version The WordPress version string. + * @global int $wp_db_version WordPress database version. + * @global string $tinymce_version TinyMCE version. + * @global string $required_php_version The required PHP version string. + * @global string[] $required_php_extensions The names of required PHP extensions. + * @global string $required_mysql_version The required MySQL version string. + * @global string $wp_local_package Locale code of the package. */ -global $wp_version, $wp_db_version, $tinymce_version, $required_php_version, $required_mysql_version, $wp_local_package; +global $wp_version, $wp_db_version, $tinymce_version, $required_php_version, $required_php_extensions, $required_mysql_version, $wp_local_package; require ABSPATH . WPINC . '/version.php'; require ABSPATH . WPINC . '/compat.php'; require ABSPATH . WPINC . '/load.php'; diff --git a/tests/phpunit/tests/compat/hashHmac.php b/tests/phpunit/tests/compat/hashHmac.php deleted file mode 100644 index f9a61d5faca63..0000000000000 --- a/tests/phpunit/tests/compat/hashHmac.php +++ /dev/null @@ -1,65 +0,0 @@ -assertTrue( function_exists( 'hash_hmac' ) ); - } - - public function test_hash_hmac_simple() { - $data = 'simple'; - $key = 'key'; - - $this->assertSame( - '140d1cb79fa12e2a31f32d35ad0a2723', - _hash_hmac( 'md5', $data, $key ), - 'MD5 hash does not match' - ); - $this->assertSame( - '993003b95758e0ac2eba451a4c5877eb1bb7b92a', - _hash_hmac( 'sha1', $data, $key ), - 'sha1 hash does not match' - ); - } - - public function test_hash_hmac_padding() { - $data = 'simple'; - $key = '65 character key 65 character key 65 character key 65 character k'; - - $this->assertSame( - '3c1399103807cf12ec38228614416a8c', - _hash_hmac( 'md5', $data, $key ), - 'MD5 hash does not match' - ); - $this->assertSame( - '4428826d20003e309d6c2a6515891370daf184ea', - _hash_hmac( 'sha1', $data, $key ), - 'sha1 hash does not match' - ); - } - - public function test_hash_hmac_output() { - $data = 'simple'; - $key = 'key'; - - $this->assertSame( - array( 1 => '140d1cb79fa12e2a31f32d35ad0a2723' ), - unpack( 'H32', _hash_hmac( 'md5', $data, $key, true ) ), - 'unpacked MD5 hash does not match' - ); - $this->assertSame( - array( 1 => '993003b95758e0ac2eba451a4c5877eb1bb7b92a' ), - unpack( 'H40', _hash_hmac( 'sha1', $data, $key, true ) ), - 'unpacked sha1 hash does not match' - ); - } -}