From c19af78a6f449ab2da0244718d7a05b61eec63fb Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 23 Jun 2026 15:58:25 -0400 Subject: [PATCH 1/6] Allow multiple query loops to be independently searchable The use of a global search context value meant that if multiple query loops were all made searchable, the last rendered or updated would "take over" all others. Moving search term scope to block context allows it to be managed independently per query loop. --- inc/namespace.php | 9 +++------ src/taxonomy/view.js | 10 ++++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/inc/namespace.php b/inc/namespace.php index 92f6386..d130cdf 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -210,21 +210,18 @@ function render_block_search( string $block_content, array $block, \WP_Block $in // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- need to preserve whitespace. $value = strip_tags( $value ); - wp_interactivity_state( 'query-filter', [ - 'searchValue' => $value, - ] ); - $block_content = new WP_HTML_Tag_Processor( $block_content ); $block_content->next_tag( [ 'tag_name' => 'form' ] ); $block_content->set_attribute( 'action', $action ); $block_content->set_attribute( 'data-wp-interactive', 'query-filter' ); $block_content->set_attribute( 'data-wp-on--submit', 'actions.search' ); - $block_content->set_attribute( 'data-wp-context', '{searchValue:""}' ); + // Scope search to block context so multiple searchable query loops may coexist. + $block_content->set_attribute( 'data-wp-context', wp_json_encode( [ 'searchValue' => $value ] ) ); $block_content->next_tag( [ 'tag_name' => 'input', 'class_name' => 'wp-block-search__input' ] ); $block_content->set_attribute( 'name', $query_var ); $block_content->set_attribute( 'inputmode', 'search' ); $block_content->set_attribute( 'value', $value ); - $block_content->set_attribute( 'data-wp-bind--value', 'state.searchValue' ); + $block_content->set_attribute( 'data-wp-bind--value', 'context.searchValue' ); $block_content->set_attribute( 'data-wp-on--input', 'actions.search' ); return (string) $block_content; diff --git a/src/taxonomy/view.js b/src/taxonomy/view.js index 2438d11..7956ac1 100644 --- a/src/taxonomy/view.js +++ b/src/taxonomy/view.js @@ -1,4 +1,4 @@ -import { store, getElement } from '@wordpress/interactivity'; +import { store, getContext, getElement } from '@wordpress/interactivity'; const updateURL = async ( action, value, name ) => { const url = new URL( action ); @@ -11,7 +11,7 @@ const updateURL = async ( action, value, name ) => { await actions.navigate( url.toString() ); }; -const { state } = store( 'query-filter', { +store( 'query-filter', { actions: { *navigate( e ) { e.preventDefault(); @@ -22,6 +22,8 @@ const { state } = store( 'query-filter', { }, *search( e ) { e.preventDefault(); + // Scope search to block context so multiple searchable query loops may coexist. + const context = getContext(); const { ref } = getElement(); let action, name, value; if ( ref.tagName === 'FORM' ) { @@ -36,9 +38,9 @@ const { state } = store( 'query-filter', { } // Don't navigate if the search didn't really change. - if ( value === state.searchValue ) return; + if ( value === context.searchValue ) return; - state.searchValue = value; + context.searchValue = value; yield updateURL( action, value, name ); }, From e39971e7fc61d34e92d9867c7aa0faf9d6098260 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 23 Jun 2026 16:25:28 -0400 Subject: [PATCH 2/6] Fix logical error where double-urldecoded search queries could mangle input Plus characters and certain % patterns would not be restored properly with this logic, which imperfectly reimplemented parts of WP Core's _sanitize_text_fields(). --- inc/namespace.php | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/inc/namespace.php b/inc/namespace.php index d130cdf..6b40411 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -180,6 +180,35 @@ function filter_block_type_metadata( array $metadata ) : array { return $metadata; } +/** + * Sanitize a search value while preserving its leading and trailing whitespace. + * + * sanitize_text_field() trims surrounding whitespace. We want to preserve that + * so that the rendered value always matches what a user is typing, such as when + * they are typing a space between words. Restore outer whitespace after sanitizing. +* + * @param string $value Raw, unslashed value. + * @return string Sanitized value with leading/trailing whitespace preserved. + */ +function sanitize_search_value( string $value ) : string { + if ( empty( $value ) ) { + return ''; + } + + $sanitized = sanitize_text_field( $value ); + + // If sanitization removed all content, echo the input back when it was + // purely whitespace or else collapse to empty string. + if ( '' === $sanitized ) { + return ctype_space( $value ) ? $value : ''; + } + + preg_match( '/^\s*/', $value, $leading ); + preg_match( '/\s*$/', $value, $trailing ); + + return $leading[0] . $sanitized . $trailing[0]; +} + /** * Filters the content of a single block. * @@ -201,14 +230,10 @@ function render_block_search( string $block_content, array $block, \WP_Block $in $action = str_replace( '/page/' . get_query_var( 'paged', 1 ), '', add_query_arg( [ $query_var => '' ] ) ); - // Note sanitize_text_field trims whitespace from start/end of string causing unexpected behaviour. - // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized - $value = wp_unslash( $_GET[ $query_var ] ?? '' ); - $value = urldecode( $value ); - $value = wp_check_invalid_utf8( $value ); - $value = wp_pre_kses_less_than( $value ); - // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- need to preserve whitespace. - $value = strip_tags( $value ); + // Sanitize via sanitize_text_field() but keep the user's leading/trailing + // whitespace so type-ahead search doesn't eat a space between words. + // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_search_value() applies sanitize_text_field(); the raw value is only read to restore outer whitespace. + $value = sanitize_search_value( wp_unslash( $_GET[ $query_var ] ?? '' ) ); $block_content = new WP_HTML_Tag_Processor( $block_content ); $block_content->next_tag( [ 'tag_name' => 'form' ] ); From 91ba04290dd897f6558286834981cba6fe0f1442 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 23 Jun 2026 16:33:47 -0400 Subject: [PATCH 3/6] Adjust signature of function to centralize all query var processing into sanitize_search_query_var() --- inc/namespace.php | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/inc/namespace.php b/inc/namespace.php index 6b40411..f7e8f80 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -181,31 +181,39 @@ function filter_block_type_metadata( array $metadata ) : array { } /** - * Sanitize a search value while preserving its leading and trailing whitespace. + * Sanitize a search query value. * * sanitize_text_field() trims surrounding whitespace. We want to preserve that * so that the rendered value always matches what a user is typing, such as when * they are typing a space between words. Restore outer whitespace after sanitizing. * - * @param string $value Raw, unslashed value. + * @param string $query_var Name of query var to capture and sanitize. * @return string Sanitized value with leading/trailing whitespace preserved. */ -function sanitize_search_value( string $value ) : string { - if ( empty( $value ) ) { +function sanitize_search_query_var( string $query_var ) : string { + if ( empty( $_GET[ $query_var ] ) ) { return ''; } - $sanitized = sanitize_text_field( $value ); + // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized -- Intermediate reference to capture whitespace only, sanitized below. + $value = wp_unslash( $_GET[ $query_var ] ); - // If sanitization removed all content, echo the input back when it was - // purely whitespace or else collapse to empty string. - if ( '' === $sanitized ) { - return ctype_space( $value ) ? $value : ''; + if ( empty( $value ) ) { + return ''; } + $sanitized = sanitize_text_field( $value ); + + // Capture surrounding whitespace. preg_match( '/^\s*/', $value, $leading ); preg_match( '/\s*$/', $value, $trailing ); + // If sanitization removed all content, leading and trailing space may be + // the same characters. Only return the trailing space, to avoid doubling. + if ( '' === $sanitized ) { + return $trailing[0]; + } + return $leading[0] . $sanitized . $trailing[0]; } @@ -230,10 +238,7 @@ function render_block_search( string $block_content, array $block, \WP_Block $in $action = str_replace( '/page/' . get_query_var( 'paged', 1 ), '', add_query_arg( [ $query_var => '' ] ) ); - // Sanitize via sanitize_text_field() but keep the user's leading/trailing - // whitespace so type-ahead search doesn't eat a space between words. - // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_search_value() applies sanitize_text_field(); the raw value is only read to restore outer whitespace. - $value = sanitize_search_value( wp_unslash( $_GET[ $query_var ] ?? '' ) ); + $search_value = sanitize_search_query_var( $query_var ); $block_content = new WP_HTML_Tag_Processor( $block_content ); $block_content->next_tag( [ 'tag_name' => 'form' ] ); @@ -241,11 +246,11 @@ function render_block_search( string $block_content, array $block, \WP_Block $in $block_content->set_attribute( 'data-wp-interactive', 'query-filter' ); $block_content->set_attribute( 'data-wp-on--submit', 'actions.search' ); // Scope search to block context so multiple searchable query loops may coexist. - $block_content->set_attribute( 'data-wp-context', wp_json_encode( [ 'searchValue' => $value ] ) ); + $block_content->set_attribute( 'data-wp-context', wp_json_encode( [ 'searchValue' => $search_value ] ) ); $block_content->next_tag( [ 'tag_name' => 'input', 'class_name' => 'wp-block-search__input' ] ); $block_content->set_attribute( 'name', $query_var ); $block_content->set_attribute( 'inputmode', 'search' ); - $block_content->set_attribute( 'value', $value ); + $block_content->set_attribute( 'value', $search_value ); $block_content->set_attribute( 'data-wp-bind--value', 'context.searchValue' ); $block_content->set_attribute( 'data-wp-on--input', 'actions.search' ); From cab60f905217c4fe0995144d66c9386b72b6cbbc Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 24 Jun 2026 09:29:11 -0400 Subject: [PATCH 4/6] Prevent valid empty() values like 0 from being incorrectly stripped --- inc/namespace.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/namespace.php b/inc/namespace.php index f7e8f80..8f54213 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -191,14 +191,14 @@ function filter_block_type_metadata( array $metadata ) : array { * @return string Sanitized value with leading/trailing whitespace preserved. */ function sanitize_search_query_var( string $query_var ) : string { - if ( empty( $_GET[ $query_var ] ) ) { + if ( ! isset( $_GET[ $query_var ] ) ) { return ''; } // phpcs:ignore HM.Security.ValidatedSanitizedInput.InputNotSanitized -- Intermediate reference to capture whitespace only, sanitized below. $value = wp_unslash( $_GET[ $query_var ] ); - if ( empty( $value ) ) { + if ( $value === '' ) { return ''; } From 5730f35a11d9de18e97558267d8a5ca2608003f8 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 24 Jun 2026 09:30:04 -0400 Subject: [PATCH 5/6] Tighten check for trailing-only return path to ensure doubling before counteracting it --- inc/namespace.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/namespace.php b/inc/namespace.php index 8f54213..8ddfe59 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -210,7 +210,7 @@ function sanitize_search_query_var( string $query_var ) : string { // If sanitization removed all content, leading and trailing space may be // the same characters. Only return the trailing space, to avoid doubling. - if ( '' === $sanitized ) { + if ( $sanitized === '' && ( $leading[0] === $trailing[0] ) ) { return $trailing[0]; } From fb84c28ad44d126b4692ed0f8a94baaec9acb275 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Wed, 24 Jun 2026 09:30:57 -0400 Subject: [PATCH 6/6] Fix docblock comment formatting in sanitize_search_query_var() --- inc/namespace.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/namespace.php b/inc/namespace.php index 8ddfe59..3a1c6a0 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -186,7 +186,7 @@ function filter_block_type_metadata( array $metadata ) : array { * sanitize_text_field() trims surrounding whitespace. We want to preserve that * so that the rendered value always matches what a user is typing, such as when * they are typing a space between words. Restore outer whitespace after sanitizing. -* + * * @param string $query_var Name of query var to capture and sanitize. * @return string Sanitized value with leading/trailing whitespace preserved. */