diff options
25 files changed, 264 insertions, 71 deletions
diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml index 83ed4d1ac7..c4bbfae729 100644 --- a/.github/workflows/reusable-test-local-docker-environment-v1.yml +++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml @@ -155,7 +155,7 @@ jobs: run: npm run env:restart - name: Test a CLI command - run: npm run env:cli wp option get siteurl + run: npm run env:cli option get siteurl - name: Test logs command run: npm run env:logs diff --git a/docker-compose.yml b/docker-compose.yml index 48f3abc607..863cbd2ea9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,10 +106,14 @@ services: PHP_FPM_UID: ${PHP_FPM_UID-1000} PHP_FPM_GID: ${PHP_FPM_GID-1000} HOST_PATH: ${PWD-}/${LOCAL_DIR-src} + WP_CONFIG_PATH: /var/www/wp-config.php volumes: - ./:/var/www + # Keeps the service alive. + command: 'sleep infinity' + # The init directive ensures the command runs with a PID > 1, so Ctrl+C works correctly. init: true diff --git a/package.json b/package.json index aeef7640f4..77c2b2d68c 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "env:clean": "node ./tools/local-env/scripts/docker.js down -v --remove-orphans", "env:reset": "node ./tools/local-env/scripts/docker.js down --rmi all -v --remove-orphans", "env:install": "node ./tools/local-env/scripts/install.js", - "env:cli": "node ./tools/local-env/scripts/docker.js run --rm cli", + "env:cli": "node ./tools/local-env/scripts/docker.js exec cli wp --allow-root", "env:logs": "node ./tools/local-env/scripts/docker.js logs", "env:pull": "node ./tools/local-env/scripts/docker.js pull", "test:performance": "wp-scripts test-playwright --config tests/performance/playwright.config.js", diff --git a/src/js/_enqueues/admin/user-profile.js b/src/js/_enqueues/admin/user-profile.js index ad808d3131..ce680ef4c4 100644 --- a/src/js/_enqueues/admin/user-profile.js +++ b/src/js/_enqueues/admin/user-profile.js @@ -101,6 +101,8 @@ return; } $toggleButton = $pass1Row.find('.wp-hide-pw'); + + // Toggle between showing and hiding the password. $toggleButton.show().on( 'click', function () { if ( 'password' === $pass1.attr( 'type' ) ) { $pass1.attr( 'type', 'text' ); @@ -110,6 +112,14 @@ resetToggle( true ); } }); + + // Ensure the password input type is set to password when the form is submitted. + $pass1Row.closest( 'form' ).on( 'submit', function() { + if ( $pass1.attr( 'type' ) === 'text' ) { + $pass1.attr( 'type', 'password' ); + resetToggle( true ); + } + } ); } /** diff --git a/src/wp-content/themes/twentynineteen/sass/navigation/_menu-main-navigation.scss b/src/wp-content/themes/twentynineteen/sass/navigation/_menu-main-navigation.scss index d1e30256f3..6d6d744ed8 100644 --- a/src/wp-content/themes/twentynineteen/sass/navigation/_menu-main-navigation.scss +++ b/src/wp-content/themes/twentynineteen/sass/navigation/_menu-main-navigation.scss @@ -433,9 +433,13 @@ white-space: inherit; } + &:not(:has(.sub-menu.expanded-true)) { + overflow-y: scroll; + } + &.expanded-true { - display: table; + display: block; margin-top: 0; opacity: 1; padding-left: 0; diff --git a/src/wp-content/themes/twentynineteen/style-rtl.css b/src/wp-content/themes/twentynineteen/style-rtl.css index da1b3636c0..9f1700c012 100644 --- a/src/wp-content/themes/twentynineteen/style-rtl.css +++ b/src/wp-content/themes/twentynineteen/style-rtl.css @@ -3271,8 +3271,12 @@ body.page .main-navigation { white-space: inherit; } +.main-navigation .main-menu .menu-item-has-children.off-canvas .sub-menu:not(:has(.sub-menu.expanded-true)) { + overflow-y: scroll; +} + .main-navigation .main-menu .menu-item-has-children.off-canvas .sub-menu.expanded-true { - display: table; + display: block; margin-top: 0; opacity: 1; padding-right: 0; diff --git a/src/wp-content/themes/twentynineteen/style.css b/src/wp-content/themes/twentynineteen/style.css index 2124cf584f..634a947b3a 100644 --- a/src/wp-content/themes/twentynineteen/style.css +++ b/src/wp-content/themes/twentynineteen/style.css @@ -3271,8 +3271,12 @@ body.page .main-navigation { white-space: inherit; } +.main-navigation .main-menu .menu-item-has-children.off-canvas .sub-menu:not(:has(.sub-menu.expanded-true)) { + overflow-y: scroll; +} + .main-navigation .main-menu .menu-item-has-children.off-canvas .sub-menu.expanded-true { - display: table; + display: block; margin-top: 0; opacity: 1; padding-left: 0; diff --git a/src/wp-includes/author-template.php b/src/wp-includes/author-template.php index 184d7d0f38..a48a6d3e6e 100644 --- a/src/wp-includes/author-template.php +++ b/src/wp-includes/author-template.php @@ -286,7 +286,7 @@ function get_the_author_posts() { if ( ! $post ) { return 0; } - return count_user_posts( $post->post_author, $post->post_type ); + return (int) count_user_posts( $post->post_author, $post->post_type ); } /** diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index 2e7c7039d5..f57e6f281f 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -305,7 +305,7 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { * image operations within the time of the HTTP request. * * @since 6.2.0 - * @since 6.3.0 This method was deprecated. + * @deprecated 6.3.0 No longer used in core. * * @return int|null The new limit on success, null on failure. */ diff --git a/src/wp-includes/class-wp-oembed.php b/src/wp-includes/class-wp-oembed.php index 2d59c2217d..43f95ed150 100644 --- a/src/wp-includes/class-wp-oembed.php +++ b/src/wp-includes/class-wp-oembed.php @@ -739,9 +739,9 @@ class WP_oEmbed { * * @since 2.9.0 * - * @param string $return The returned oEmbed HTML. - * @param object $data A data object result from an oEmbed provider. - * @param string $url The URL of the content to be embedded. + * @param string|false $return The returned oEmbed HTML, or false on failure. + * @param object $data A data object result from an oEmbed provider. + * @param string $url The URL of the content to be embedded. */ return apply_filters( 'oembed_dataparse', $return, $data, $url ); } @@ -752,10 +752,10 @@ class WP_oEmbed { * @since 2.9.0 as strip_scribd_newlines() * @since 3.0.0 * - * @param string $html Existing HTML. - * @param object $data Data object from WP_oEmbed::data2html() - * @param string $url The original URL passed to oEmbed. - * @return string Possibly modified $html + * @param string|false $html Existing HTML. + * @param object $data Data object from WP_oEmbed::data2html() + * @param string $url The original URL passed to oEmbed. + * @return string|false Possibly modified $html. */ public function _strip_newlines( $html, $data, $url ) { if ( ! str_contains( $html, "\n" ) ) { diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index cd41d4b200..f023c03cd0 100644 --- a/src/wp-includes/comment-template.php +++ b/src/wp-includes/comment-template.php @@ -2446,6 +2446,7 @@ function wp_list_comments( $args = array(), $comments = null ) { * @since 4.6.0 Introduced the 'action' argument. * @since 4.9.6 Introduced the 'cookies' default comment field. * @since 5.5.0 Introduced the 'class_container' argument. + * @since 6.8.2 Introduced the 'novalidate' argument. * * @param array $args { * Optional. Default arguments and form fields to override. @@ -2467,6 +2468,7 @@ function wp_list_comments( $args = array(), $comments = null ) { * Default 'Your email address will not be published.'. * @type string $comment_notes_after HTML element for a message displayed after the textarea field. * @type string $action The comment form element action attribute. Default '/wp-comments-post.php'. + * @type bool $novalidate Whether the novalidate attribute is added to the comment form. Default false. * @type string $id_form The comment form element id attribute. Default 'commentform'. * @type string $id_submit The comment submit element id attribute. Default 'submit'. * @type string $class_container The comment form container class attribute. Default 'comment-respond'. @@ -2646,6 +2648,7 @@ function comment_form( $args = array(), $post = null ) { ), 'comment_notes_after' => '', 'action' => site_url( '/wp-comments-post.php' ), + 'novalidate' => false, 'id_form' => 'commentform', 'id_submit' => 'submit', 'class_container' => 'comment-respond', @@ -2729,7 +2732,7 @@ function comment_form( $args = array(), $post = null ) { esc_url( $args['action'] ), esc_attr( $args['id_form'] ), esc_attr( $args['class_form'] ), - ( $html5 ? ' novalidate' : '' ) + ( $args['novalidate'] ? ' novalidate' : '' ) ); /** diff --git a/src/wp-includes/embed.php b/src/wp-includes/embed.php index b5b30acead..a3c23be931 100644 --- a/src/wp-includes/embed.php +++ b/src/wp-includes/embed.php @@ -843,10 +843,10 @@ function _oembed_create_xml( $data, $node = null ) { * * @since 5.2.0 * - * @param string $result The oEmbed HTML result. - * @param object $data A data object result from an oEmbed provider. - * @param string $url The URL of the content to be embedded. - * @return string The filtered oEmbed result. + * @param string|false $result The oEmbed HTML result. + * @param object $data A data object result from an oEmbed provider. + * @param string $url The URL of the content to be embedded. + * @return string|false The filtered oEmbed result. */ function wp_filter_oembed_iframe_title_attribute( $result, $data, $url ) { if ( false === $result || ! in_array( $data->type, array( 'rich', 'video' ), true ) ) { @@ -910,10 +910,10 @@ function wp_filter_oembed_iframe_title_attribute( $result, $data, $url ) { * * @since 4.4.0 * - * @param string $result The oEmbed HTML result. - * @param object $data A data object result from an oEmbed provider. - * @param string $url The URL of the content to be embedded. - * @return string The filtered and sanitized oEmbed result. + * @param string|false $result The oEmbed HTML result. + * @param object $data A data object result from an oEmbed provider. + * @param string $url The URL of the content to be embedded. + * @return string|false The filtered and sanitized oEmbed result. */ function wp_filter_oembed_result( $result, $data, $url ) { if ( false === $result || ! in_array( $data->type, array( 'rich', 'video' ), true ) ) { diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 9fd6d1d00d..1dbac5e1d7 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -2676,9 +2676,11 @@ if ( ! function_exists( 'wp_hash_password' ) ) : * - `PASSWORD_ARGON2ID` * - `PASSWORD_DEFAULT` * + * The values of the algorithm constants are strings in PHP 7.4+ and integers in PHP 7.3 and earlier. + * * @since 6.8.0 * - * @param string $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant. + * @param string|int $algorithm The hashing algorithm. Default is the value of the `PASSWORD_BCRYPT` constant. */ $algorithm = apply_filters( 'wp_hash_password_algorithm', PASSWORD_BCRYPT ); @@ -2688,12 +2690,14 @@ if ( ! function_exists( 'wp_hash_password' ) ) : * The default hashing algorithm is bcrypt, but this can be changed via the {@see 'wp_hash_password_algorithm'} * filter. You must ensure that the options are appropriate for the algorithm in use. * + * The values of the algorithm constants are strings in PHP 7.4+ and integers in PHP 7.3 and earlier. + * * @since 6.8.0 * - * @param array $options Array of options to pass to the password hashing functions. - * By default this is an empty array which means the default - * options will be used. - * @param string $algorithm The hashing algorithm in use. + * @param array $options Array of options to pass to the password hashing functions. + * By default this is an empty array which means the default + * options will be used. + * @param string|int $algorithm The hashing algorithm in use. */ $options = apply_filters( 'wp_hash_password_options', array(), $algorithm ); diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index ae711eebb8..b312ac394b 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -6877,7 +6877,9 @@ function wp_get_attachment_metadata( $attachment_id = 0, $unfiltered = false ) { * * @param int $attachment_id Attachment post ID. * @param array $data Attachment meta data. - * @return int|false False if $post is invalid. + * @return int|bool Whether the metadata was successfully updated. + * True on success, the Meta ID if the key didn't exist. + * False if $post is invalid, on failure, or if $data is the same as the existing metadata. */ function wp_update_attachment_metadata( $attachment_id, $data ) { $attachment_id = (int) $attachment_id; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php index 004f5851a2..66cf8785e4 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php @@ -147,6 +147,18 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { $params = $request->get_params(); + if ( empty( $params ) || ! empty( array_diff_key( $params, $options ) ) ) { + $message = empty( $params ) + ? __( 'Request body cannot be empty.' ) + : __( 'Invalid parameter(s) provided.' ); + + return new WP_Error( + 'rest_invalid_param', + $message, + array( 'status' => 400 ) + ); + } + foreach ( $options as $name => $args ) { if ( ! array_key_exists( $name, $params ) ) { continue; diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 083de80304..4dacf58628 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -637,7 +637,7 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false ) * @since 4.1.0 Added `$post_type` argument. * @since 4.3.1 Added `$public_only` argument. * - * @param int $count The user's post count. + * @param string $count The user's post count as a numeric string. * @param int $userid User ID. * @param string|array $post_type Single post type or array of post types to count the number of posts for. * @param bool $public_only Whether to limit counted posts to public posts. diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 6750df13c7..29ce8d8b53 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -136,6 +136,21 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase { $this->start_transaction(); $this->expectDeprecated(); add_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) ); + add_filter( 'wp_hash_password_options', array( $this, 'wp_hash_password_options' ), 1, 2 ); + } + + /** + * Sets the bcrypt cost option for password hashing during tests. + * + * @param array $options The options for password hashing. + * @param string|int $algorithm The algorithm to use for hashing. This is a string in PHP 7.4+ and an integer in PHP 7.3 and earlier. + */ + public function wp_hash_password_options( array $options, $algorithm ): array { + if ( PASSWORD_BCRYPT === $algorithm ) { + $options['cost'] = 5; + } + + return $options; } /** diff --git a/tests/phpunit/tests/auth.php b/tests/phpunit/tests/auth.php index 405a8526d0..a490842ebd 100644 --- a/tests/phpunit/tests/auth.php +++ b/tests/phpunit/tests/auth.php @@ -2089,9 +2089,6 @@ class Tests_Auth extends WP_UnitTestCase { } private static function get_default_bcrypt_cost(): int { - $hash = password_hash( 'password', PASSWORD_BCRYPT ); - $info = password_get_info( $hash ); - - return $info['options']['cost']; + return 5; } } diff --git a/tests/phpunit/tests/comment/commentForm.php b/tests/phpunit/tests/comment/commentForm.php index 771cbc1b57..e3dab07e24 100644 --- a/tests/phpunit/tests/comment/commentForm.php +++ b/tests/phpunit/tests/comment/commentForm.php @@ -193,4 +193,38 @@ class Tests_Comment_CommentForm extends WP_UnitTestCase { $post_hidden_field = "<input type='hidden' name='comment_post_ID' value='{$post_id}' id='comment_post_ID' />"; $this->assertStringContainsString( $post_hidden_field, $form ); } + + /** + * Tests novalidate attribute on the comment form. + * + * @ticket 47595 + */ + public function test_comment_form_and_novalidate_attribute() { + $post_id = self::$post_id; + + // By default, the novalidate is not emitted. + $form = get_echo( 'comment_form', array( array(), $post_id ) ); + $p = new WP_HTML_Tag_Processor( $form ); + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'FORM' ) ), 'Expected FORM tag.' ); + $this->assertNull( $p->get_attribute( 'novalidate' ), 'Expected FORM to not have novalidate attribute by default.' ); + + // Opt in to the novalidate attribute by passing an arg to comment_form(). + $form = get_echo( 'comment_form', array( array( 'novalidate' => true ), $post_id ) ); + $p = new WP_HTML_Tag_Processor( $form ); + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'FORM' ) ), 'Expected FORM tag.' ); + $this->assertTrue( $p->get_attribute( 'novalidate' ), 'Expected FORM to have the novalidate attribute.' ); + + // Opt in to the novalidate attribute via the comment_form_defaults filter. + add_filter( + 'comment_form_defaults', + static function ( array $defaults ): array { + $defaults['novalidate'] = true; + return $defaults; + } + ); + $form = get_echo( 'comment_form', array( array(), $post_id ) ); + $p = new WP_HTML_Tag_Processor( $form ); + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'FORM' ) ), 'Expected FORM tag.' ); + $this->assertTrue( $p->get_attribute( 'novalidate' ), 'Expected FORM to have novalidate attribute.' ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index e8f90b53f2..2e5e978655 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -385,14 +385,21 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase } /** - * @doesNotPerformAssertions + * Settings can't be created */ public function test_create_item() { - // Controller does not implement create_item(). + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/settings' ); + $request->set_param( 'new_setting', 'New value' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 400, $response->get_status() ); } public function test_update_item() { wp_set_current_user( self::$administrator ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); $request->set_param( 'title', 'The new title!' ); $response = rest_get_server()->dispatch( $request ); @@ -403,6 +410,27 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase $this->assertSame( get_option( 'blogname' ), $data['title'] ); } + public function test_update_nonexistent_item() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'i_do_no_exist', 'New value' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 400, $response->get_status() ); + } + + public function test_update_partially_valid_items() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'title', 'The new title!' ); + $request->set_param( 'i_do_no_exist', 'New value' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 400, $response->get_status() ); + } + public function update_setting_custom_callback( $result, $name, $value, $args ) { if ( 'title' === $name && 'The new title!' === $value ) { // Do not allow changing the title in this case. diff --git a/tests/phpunit/tests/user/getTheAuthorPosts.php b/tests/phpunit/tests/user/getTheAuthorPosts.php index 5cd87bc79b..3a0abac5de 100644 --- a/tests/phpunit/tests/user/getTheAuthorPosts.php +++ b/tests/phpunit/tests/user/getTheAuthorPosts.php @@ -42,7 +42,7 @@ class Tests_User_GetTheAuthorPosts extends WP_UnitTestCase { // Test with no global post, result should be 0 because no author is found. $this->assertSame( 0, get_the_author_posts() ); $GLOBALS['post'] = self::$post_id; - $this->assertEquals( 1, get_the_author_posts() ); + $this->assertSame( 1, get_the_author_posts() ); } /** @@ -60,7 +60,7 @@ class Tests_User_GetTheAuthorPosts extends WP_UnitTestCase { ); $GLOBALS['post'] = $cpt_ids[0]; - $this->assertEquals( 2, get_the_author_posts() ); + $this->assertSame( 2, get_the_author_posts() ); _unregister_post_type( 'wptests_pt' ); } diff --git a/tools/local-env/scripts/docker.js b/tools/local-env/scripts/docker.js index c1dc2b27e1..e39b42a812 100644 --- a/tools/local-env/scripts/docker.js +++ b/tools/local-env/scripts/docker.js @@ -1,21 +1,36 @@ -const dotenv = require( 'dotenv' ); +/* jshint node:true */ + +const dotenv = require( 'dotenv' ); const dotenvExpand = require( 'dotenv-expand' ); -const { execSync } = require( 'child_process' ); +const { spawnSync } = require( 'child_process' ); const local_env_utils = require( './utils' ); dotenvExpand.expand( dotenv.config() ); const composeFiles = local_env_utils.get_compose_files(); -if (process.argv.includes('--coverage-html')) { +if ( process.argv.includes( '--coverage-html' ) ) { process.env.LOCAL_PHP_XDEBUG = 'true'; process.env.LOCAL_PHP_XDEBUG_MODE = 'coverage'; } -// This try-catch prevents the superfluous Node.js debugging information from being shown if the command fails. -try { - // Execute any Docker compose command passed to this script. - execSync( 'docker compose ' + composeFiles + ' ' + process.argv.slice( 2 ).join( ' ' ), { stdio: 'inherit' } ); -} catch ( error ) { - process.exit( 1 ); +// Add --no-TTY (-T) arg after exec and run commands when STDIN is not a TTY. +const dockerCommand = process.argv.slice( 2 ); +if ( [ 'exec', 'run' ].includes( dockerCommand[0] ) && ! process.stdin.isTTY ) { + dockerCommand.splice( 1, 0, '--no-TTY' ); } + +// Execute any Docker compose command passed to this script. +const returns = spawnSync( + 'docker', + [ + 'compose', + ...composeFiles + .map( ( composeFile ) => [ '-f', composeFile ] ) + .flat(), + ...dockerCommand, + ], + { stdio: 'inherit' } +); + +process.exit( returns.status ); diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 3bbc30d4d8..19a0f46e08 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -1,8 +1,10 @@ +/* jshint node:true */ + const dotenv = require( 'dotenv' ); const dotenvExpand = require( 'dotenv-expand' ); const wait_on = require( 'wait-on' ); const { execSync } = require( 'child_process' ); -const { renameSync, readFileSync, writeFileSync } = require( 'fs' ); +const { readFileSync, writeFileSync } = require( 'fs' ); const local_env_utils = require( './utils' ); dotenvExpand.expand( dotenv.config() ); @@ -11,7 +13,10 @@ dotenvExpand.expand( dotenv.config() ); local_env_utils.determine_auth_option(); // Create wp-config.php. -wp_cli( 'config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force' ); +wp_cli( `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force --config-file="wp-config.php"` ); + +// Since WP-CLI runs as root, the wp-config.php created above will be read-only. This needs to be writable for the sake of E2E tests. +execSync( 'node ./tools/local-env/scripts/docker.js exec cli chmod 666 wp-config.php' ); // Add the debug settings to wp-config.php. // Windows requires this to be done as an additional step, rather than using the --extra-php option in the previous step. @@ -22,26 +27,35 @@ wp_cli( `config set SCRIPT_DEBUG ${process.env.LOCAL_SCRIPT_DEBUG} --raw --type= wp_cli( `config set WP_ENVIRONMENT_TYPE ${process.env.LOCAL_WP_ENVIRONMENT_TYPE} --type=constant` ); wp_cli( `config set WP_DEVELOPMENT_MODE ${process.env.LOCAL_WP_DEVELOPMENT_MODE} --type=constant` ); -// Move wp-config.php to the base directory, so it doesn't get mixed up in the src or build directories. -renameSync( `${process.env.LOCAL_DIR}/wp-config.php`, 'wp-config.php' ); - // Read in wp-tests-config-sample.php, edit it to work with our config, then write it to wp-tests-config.php. const testConfig = readFileSync( 'wp-tests-config-sample.php', 'utf8' ) .replace( 'youremptytestdbnamehere', 'wordpress_develop_tests' ) .replace( 'yourusernamehere', 'root' ) .replace( 'yourpasswordhere', 'password' ) .replace( 'localhost', 'mysql' ) - .replace( "'WP_TESTS_DOMAIN', 'example.org'", `'WP_TESTS_DOMAIN', '${process.env.LOCAL_WP_TESTS_DOMAIN}'` ) - .concat( "\ndefine( 'FS_METHOD', 'direct' );\n" ); + .replace( `'WP_TESTS_DOMAIN', 'example.org'`, `'WP_TESTS_DOMAIN', '${process.env.LOCAL_WP_TESTS_DOMAIN}'` ) + .concat( `\ndefine( 'FS_METHOD', 'direct' );\n` ); writeFileSync( 'wp-tests-config.php', testConfig ); // Once the site is available, install WordPress! -wait_on( { resources: [ `tcp:localhost:${process.env.LOCAL_PORT}`] } ) +wait_on( { + resources: [ `tcp:localhost:${process.env.LOCAL_PORT}`], + timeout: 3000, +} ) + .catch( err => { + console.error( `Error: It appears the development environment has not been started. Message: ${ err.message }` ); + console.error( `Did you forget to do 'npm run env:start'?` ); + process.exit( 1 ); + } ) .then( () => { wp_cli( 'db reset --yes' ); const installCommand = process.env.LOCAL_MULTISITE === 'true' ? 'multisite-install' : 'install'; wp_cli( `core ${ installCommand } --title="WordPress Develop" --admin_user=admin --admin_password=password --admin_email=test@example.com --skip-email --url=http://localhost:${process.env.LOCAL_PORT}` ); + } ) + .catch( err => { + console.error( `Error: Unable to reset DB and install WordPress. Message: ${ err.message }` ); + process.exit( 1 ); } ); /** @@ -50,7 +64,5 @@ wait_on( { resources: [ `tcp:localhost:${process.env.LOCAL_PORT}`] } ) * @param {string} cmd The WP-CLI command to run. */ function wp_cli( cmd ) { - const composeFiles = local_env_utils.get_compose_files(); - - execSync( `docker compose ${composeFiles} run --quiet-pull --rm cli ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio: 'inherit' } ); + execSync( `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio: 'inherit' } ); } diff --git a/tools/local-env/scripts/start.js b/tools/local-env/scripts/start.js index 0dc8b95700..b0389b2fb0 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -1,11 +1,13 @@ +/* jshint node:true */ + const dotenv = require( 'dotenv' ); const dotenvExpand = require( 'dotenv-expand' ); -const { execSync } = require( 'child_process' ); +const { execSync, spawnSync } = require( 'child_process' ); const local_env_utils = require( './utils' ); const { constants, copyFile } = require( 'node:fs' ); // Copy the default .env file when one is not present. -copyFile( '.env.example', '.env', constants.COPYFILE_EXCL, (e) => { +copyFile( '.env.example', '.env', constants.COPYFILE_EXCL, () => { console.log( '.env file already exists. .env.example was not copied.' ); }); @@ -28,18 +30,38 @@ try { } // Start the local-env containers. -const containers = ( process.env.LOCAL_PHP_MEMCACHED === 'true' ) - ? 'wordpress-develop memcached' - : 'wordpress-develop'; -execSync( `docker compose ${composeFiles} up --quiet-pull -d ${containers}`, { stdio: 'inherit' } ); +const containers = [ 'wordpress-develop', 'cli' ]; +if ( process.env.LOCAL_PHP_MEMCACHED === 'true' ) { + containers.push( 'memcached' ); +} + +spawnSync( + 'docker', + [ + 'compose', + ...composeFiles.map( ( composeFile ) => [ '-f', composeFile ] ).flat(), + 'up', + '--quiet-pull', + '-d', + ...containers, + ], + { stdio: 'inherit' } +); // If Docker Toolbox is being used, we need to manually forward LOCAL_PORT to the Docker VM. if ( process.env.DOCKER_TOOLBOX_INSTALL_PATH ) { // VBoxManage is added to the PATH on every platform except Windows. - const vboxmanage = process.env.VBOX_MSI_INSTALL_PATH ? `${ process.env.VBOX_MSI_INSTALL_PATH }/VBoxManage` : 'VBoxManage' + const vboxmanage = process.env.VBOX_MSI_INSTALL_PATH ? `${ process.env.VBOX_MSI_INSTALL_PATH }/VBoxManage` : 'VBoxManage'; // Check if the port forwarding is already configured for this port. - const vminfoBuffer = execSync( `"${ vboxmanage }" showvminfo "${ process.env.DOCKER_MACHINE_NAME }" --machinereadable` ); + const vminfoBuffer = spawnSync( + vboxmanage, + [ + 'showvminfo', + process.env.DOCKER_MACHINE_NAME, + '--machinereadable' + ] + ).stdout; const vminfo = vminfoBuffer.toString().split( /[\r\n]+/ ); vminfo.forEach( ( info ) => { @@ -53,10 +75,29 @@ if ( process.env.DOCKER_TOOLBOX_INSTALL_PATH ) { // Delete rules that are using the port we need. if ( rule[ 3 ] === process.env.LOCAL_PORT || rule[ 5 ] === process.env.LOCAL_PORT ) { - execSync( `"${ vboxmanage }" controlvm "${ process.env.DOCKER_MACHINE_NAME }" natpf1 delete ${ rule[ 0 ] }`, { stdio: 'inherit' } ); + spawnSync( + vboxmanage, + [ + 'controlvm', + process.env.DOCKER_MACHINE_NAME, + 'natpf1', + 'delete', + rule[ 0 ] + ], + { stdio: 'inherit' } + ); } } ); // Add our port forwarding rule. - execSync( `"${ vboxmanage }" controlvm "${ process.env.DOCKER_MACHINE_NAME }" natpf1 "tcp-port${ process.env.LOCAL_PORT },tcp,127.0.0.1,${ process.env.LOCAL_PORT },,${ process.env.LOCAL_PORT }"`, { stdio: 'inherit' } ); + spawnSync( + vboxmanage, + [ + 'controlvm', + process.env.DOCKER_MACHINE_NAME, + 'natpf1', + `tcp-port${ process.env.LOCAL_PORT },tcp,127.0.0.1,${ process.env.LOCAL_PORT },,${ process.env.LOCAL_PORT }` + ], + { stdio: 'inherit' } + ); } diff --git a/tools/local-env/scripts/utils.js b/tools/local-env/scripts/utils.js index d76f3068a5..3f3e601db2 100644 --- a/tools/local-env/scripts/utils.js +++ b/tools/local-env/scripts/utils.js @@ -1,3 +1,5 @@ +/* jshint node:true */ + const { existsSync } = require( 'node:fs' ); const local_env_utils = { @@ -10,12 +12,14 @@ const local_env_utils = { * * When PHP 7.2 or 7.3 is used in combination with MySQL 8.4, an override file will also be returned to ensure * that the mysql_native_password plugin authentication plugin is on and available for use. + * + * @return {string[]} Compose files. */ get_compose_files: function() { - var composeFiles = '-f docker-compose.yml'; + const composeFiles = [ 'docker-compose.yml' ]; if ( existsSync( 'docker-compose.override.yml' ) ) { - composeFiles = composeFiles + ' -f docker-compose.override.yml'; + composeFiles.push( 'docker-compose.override.yml' ); } if ( process.env.LOCAL_DB_TYPE !== 'mysql' ) { @@ -28,7 +32,7 @@ const local_env_utils = { // PHP 7.2/7.3 in combination with MySQL 8.4 requires additional configuration to function properly. if ( process.env.LOCAL_DB_VERSION === '8.4' ) { - composeFiles = composeFiles + ' -f tools/local-env/old-php-mysql-84.override.yml'; + composeFiles.push( 'tools/local-env/old-php-mysql-84.override.yml' ); } return composeFiles; |