diff --git a/src/wp-includes/block-supports/auto-register.php b/src/wp-includes/block-supports/auto-register.php new file mode 100644 index 0000000000000..83feedaaeeade --- /dev/null +++ b/src/wp-includes/block-supports/auto-register.php @@ -0,0 +1,61 @@ + $args Array of arguments for registering a block type. + * @return array Modified block type arguments. + */ +function wp_mark_auto_generate_control_attributes( array $args ): array { + if ( empty( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { + return $args; + } + + $has_auto_register = ! empty( $args['supports']['autoRegister'] ); + if ( ! $has_auto_register ) { + return $args; + } + + foreach ( $args['attributes'] as $attr_key => $attr_schema ) { + // Skip HTML-derived attributes (edited inline, not via inspector). + if ( ! empty( $attr_schema['source'] ) ) { + continue; + } + // Skip internal attributes (not user-configurable). + if ( isset( $attr_schema['role'] ) && 'local' === $attr_schema['role'] ) { + continue; + } + // Skip unsupported types (only 'string', 'number', 'integer', 'boolean' are supported). + $type = $attr_schema['type'] ?? null; + if ( ! in_array( $type, array( 'string', 'number', 'integer', 'boolean' ), true ) ) { + continue; + } + $args['attributes'][ $attr_key ]['autoGenerateControl'] = true; + } + + return $args; +} + +// Priority 5 to mark original attributes before other filters (priority 10+) might add their own. +add_filter( 'register_block_type_args', 'wp_mark_auto_generate_control_attributes', 5 ); diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index e7042b8266e54..89007d0d0d036 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -3131,3 +3131,31 @@ function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { } return $arg; } + +/** + * Exposes blocks with autoRegister flag for ServerSideRender in the editor. + * + * Detects blocks that have the autoRegister flag set in their supports + * and passes them to JavaScript for auto-registration with ServerSideRender. + * + * @access private + * @since 7.0.0 + */ +function _wp_enqueue_auto_register_blocks() { + $auto_register_blocks = array(); + $registered_blocks = WP_Block_Type_Registry::get_instance()->get_all_registered(); + + foreach ( $registered_blocks as $block_name => $block_type ) { + if ( ! empty( $block_type->supports['autoRegister'] ) && ! empty( $block_type->render_callback ) ) { + $auto_register_blocks[] = $block_name; + } + } + + if ( ! empty( $auto_register_blocks ) ) { + wp_add_inline_script( + 'wp-block-library', + sprintf( 'window.__unstableAutoRegisterBlocks = %s;', wp_json_encode( $auto_register_blocks ) ), + 'before' + ); + } +} diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index de0b374ef4b56..cc010a7b62202 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -623,6 +623,7 @@ add_action( 'enqueue_block_editor_assets', 'wp_enqueue_editor_format_library_assets' ); add_action( 'enqueue_block_editor_assets', 'wp_enqueue_block_editor_script_modules' ); add_action( 'enqueue_block_editor_assets', 'wp_enqueue_global_styles_css_custom_properties' ); +add_action( 'enqueue_block_editor_assets', '_wp_enqueue_auto_register_blocks' ); add_action( 'wp_print_scripts', 'wp_just_in_time_script_localization' ); add_filter( 'print_scripts_array', 'wp_prototype_before_jquery' ); add_action( 'customize_controls_print_styles', 'wp_resource_hints', 1 ); diff --git a/src/wp-settings.php b/src/wp-settings.php index 60c220100f539..0014dc692200d 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -394,6 +394,7 @@ require ABSPATH . WPINC . '/class-wp-block-supports.php'; require ABSPATH . WPINC . '/block-supports/utils.php'; require ABSPATH . WPINC . '/block-supports/align.php'; +require ABSPATH . WPINC . '/block-supports/auto-register.php'; require ABSPATH . WPINC . '/block-supports/custom-classname.php'; require ABSPATH . WPINC . '/block-supports/generated-classname.php'; require ABSPATH . WPINC . '/block-supports/settings.php'; diff --git a/tests/phpunit/tests/block-supports/auto-register.php b/tests/phpunit/tests/block-supports/auto-register.php new file mode 100644 index 0000000000000..71754f247225b --- /dev/null +++ b/tests/phpunit/tests/block-supports/auto-register.php @@ -0,0 +1,144 @@ + array( 'autoRegister' => true ), + 'attributes' => array( + 'title' => array( 'type' => 'string' ), + 'count' => array( 'type' => 'integer' ), + ), + ); + + $result = wp_mark_auto_generate_control_attributes( $settings ); + + $this->assertTrue( $result['attributes']['title']['autoGenerateControl'] ); + $this->assertTrue( $result['attributes']['count']['autoGenerateControl'] ); + } + + /** + * Tests that attributes are not marked without autoRegister flag. + * + * @ticket 64639 + */ + public function test_does_not_mark_attributes_without_auto_register() { + $settings = array( + 'attributes' => array( + 'title' => array( 'type' => 'string' ), + ), + ); + + $result = wp_mark_auto_generate_control_attributes( $settings ); + + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['title'] ); + } + + /** + * Tests that attributes with source are excluded. + * + * @ticket 64639 + */ + public function test_excludes_attributes_with_source() { + $settings = array( + 'supports' => array( 'autoRegister' => true ), + 'attributes' => array( + 'title' => array( 'type' => 'string' ), + 'content' => array( + 'type' => 'string', + 'source' => 'html', + ), + ), + ); + + $result = wp_mark_auto_generate_control_attributes( $settings ); + + $this->assertTrue( $result['attributes']['title']['autoGenerateControl'] ); + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['content'] ); + } + + /** + * Tests that attributes with role: local are excluded. + * + * Example: The 'blob' attribute in media blocks (image, video, file, audio) + * stores a temporary blob URL during file upload. This is internal state + * that shouldn't be shown in the inspector or saved to the database. + * + * @ticket 64639 + */ + public function test_excludes_attributes_with_role_local() { + $settings = array( + 'supports' => array( 'autoRegister' => true ), + 'attributes' => array( + 'title' => array( 'type' => 'string' ), + 'blob' => array( + 'type' => 'string', + 'role' => 'local', + ), + ), + ); + + $result = wp_mark_auto_generate_control_attributes( $settings ); + + $this->assertTrue( $result['attributes']['title']['autoGenerateControl'] ); + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['blob'] ); + } + + /** + * Tests that empty attributes are handled gracefully. + * + * @ticket 64639 + */ + public function test_handles_empty_attributes() { + $settings = array( + 'supports' => array( 'autoRegister' => true ), + ); + + $result = wp_mark_auto_generate_control_attributes( $settings ); + + $this->assertSame( $settings, $result ); + } + + /** + * Tests that only allowed attributes are marked. + * + * @ticket 64639 + */ + public function test_excludes_unsupported_types() { + $settings = array( + 'supports' => array( 'autoRegister' => true ), + 'attributes' => array( + // Supported types + 'text' => array( 'type' => 'string' ), + 'price' => array( 'type' => 'number' ), + 'count' => array( 'type' => 'integer' ), + 'enabled' => array( 'type' => 'boolean' ), + // Unsupported types + 'metadata' => array( 'type' => 'object' ), + 'items' => array( 'type' => 'array' ), + 'config' => array( 'type' => 'null' ), + 'unknown' => array( 'type' => 'unknown' ), + ), + ); + + $result = wp_mark_auto_generate_control_attributes( $settings ); + + $this->assertTrue( $result['attributes']['text']['autoGenerateControl'] ); + $this->assertTrue( $result['attributes']['price']['autoGenerateControl'] ); + $this->assertTrue( $result['attributes']['count']['autoGenerateControl'] ); + $this->assertTrue( $result['attributes']['enabled']['autoGenerateControl'] ); + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['metadata'] ); + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['items'] ); + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['config'] ); + $this->assertArrayNotHasKey( 'autoGenerateControl', $result['attributes']['unknown'] ); + } +}