Skip to content

Commit c8a73cf

Browse files
committed
Add validation warnings to bind()
Trigger _doing_it_wrong() for: - Missing replacement keys (placeholder without value) - Unused replacement keys (value without placeholder) - Template values in attribute context Share compiled data between original and bound template instances for efficiency.
1 parent f837c75 commit c8a73cf

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

src/wp-includes/html-api/class-wp-html-template.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,98 @@ public static function from( string $template ): static {
183183
/**
184184
* Returns a new immutable instance with replacements bound.
185185
*
186+
* Triggers compilation if not already done. Validates replacements:
187+
* - Warns if a placeholder has no corresponding replacement
188+
* - Warns if a replacement key has no corresponding placeholder
189+
* - Warns if a template is used in attribute context
190+
*
186191
* @since 7.0.0
187192
*
188193
* @param array $replacements The replacement values.
189194
* @return static A new template instance with the replacements bound.
190195
*/
191196
public function bind( array $replacements ): static {
192-
return new static( $this->template_string, $replacements );
197+
$this->compile();
198+
199+
// Build a lookup of placeholder keys from compiled data.
200+
$placeholder_keys = array();
201+
foreach ( $this->compiled as $placeholder => $info ) {
202+
$placeholder = (string) $placeholder;
203+
$placeholder_keys[ $placeholder ] = true;
204+
if ( ctype_digit( $placeholder ) ) {
205+
$placeholder_keys[ (int) $placeholder ] = true;
206+
}
207+
}
208+
209+
// Build a lookup of replacement keys.
210+
$replacement_keys = array();
211+
foreach ( $replacements as $key => $value ) {
212+
$replacement_keys[ (string) $key ] = true;
213+
if ( is_int( $key ) ) {
214+
$replacement_keys[ $key ] = true;
215+
}
216+
}
217+
218+
// Check for missing keys (placeholder without replacement).
219+
foreach ( $this->compiled as $placeholder => $info ) {
220+
$placeholder = (string) $placeholder;
221+
$found = isset( $replacement_keys[ $placeholder ] );
222+
if ( ! $found && ctype_digit( $placeholder ) ) {
223+
$found = isset( $replacement_keys[ (int) $placeholder ] ) || array_key_exists( (int) $placeholder, $replacements );
224+
}
225+
if ( ! $found ) {
226+
_doing_it_wrong(
227+
__METHOD__,
228+
sprintf(
229+
'Missing replacement for placeholder: %s',
230+
$placeholder
231+
),
232+
'7.0.0'
233+
);
234+
}
235+
}
236+
237+
// Check for unused keys (replacement without placeholder).
238+
foreach ( $replacements as $key => $value ) {
239+
$str_key = (string) $key;
240+
$found = isset( $placeholder_keys[ $key ] ) || isset( $placeholder_keys[ $str_key ] );
241+
if ( ! $found ) {
242+
_doing_it_wrong(
243+
__METHOD__,
244+
sprintf(
245+
'Unused replacement key: %s',
246+
$key
247+
),
248+
'7.0.0'
249+
);
250+
}
251+
}
252+
253+
// Check for templates in attribute context.
254+
foreach ( $this->compiled as $placeholder => $info ) {
255+
$placeholder = (string) $placeholder;
256+
if ( 'attribute' !== $info['context'] ) {
257+
continue;
258+
}
259+
260+
$key = ctype_digit( $placeholder ) ? (int) $placeholder : $placeholder;
261+
$value = $replacements[ $key ] ?? $replacements[ $placeholder ] ?? null;
262+
263+
if ( $value instanceof self ) {
264+
_doing_it_wrong(
265+
__METHOD__,
266+
sprintf(
267+
'Template cannot be used in attribute context: %s',
268+
$placeholder
269+
),
270+
'7.0.0'
271+
);
272+
}
273+
}
274+
275+
$new = new static( $this->template_string, $replacements );
276+
$new->compiled = $this->compiled;
277+
return $new;
193278
}
194279

195280
/**

tests/phpunit/tests/html-api/wpHtmlTemplate.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public function test_escapes_ampersand_to_prevent_character_reference_injection(
135135
* @covers ::from
136136
* @covers ::bind
137137
* @covers ::render
138+
*
139+
* @expectedIncorrectUsage WP_HTML_Template::bind
138140
*/
139141
public function test_rejects_nested_template_in_attribute_value() {
140142
$template_string = '<meta name="not-allowed" description="</%html>">';
@@ -755,6 +757,48 @@ public static function data_pre_element_leading_newline() {
755757
);
756758
}
757759

760+
/**
761+
* Verifies bind() warns on missing replacement key.
762+
*
763+
* @ticket 60229
764+
*
765+
* @covers ::bind
766+
*
767+
* @expectedIncorrectUsage WP_HTML_Template::bind
768+
*/
769+
public function test_bind_warns_on_missing_key() {
770+
$template = T::from( '<p></%name> </%age></p>' );
771+
$template->bind( array( 'name' => 'Alice' ) );
772+
}
773+
774+
/**
775+
* Verifies bind() warns on unused replacement key.
776+
*
777+
* @ticket 60229
778+
*
779+
* @covers ::bind
780+
*
781+
* @expectedIncorrectUsage WP_HTML_Template::bind
782+
*/
783+
public function test_bind_warns_on_unused_key() {
784+
$template = T::from( '<p></%name></p>' );
785+
$template->bind( array( 'name' => 'Alice', 'extra' => 'ignored' ) );
786+
}
787+
788+
/**
789+
* Verifies bind() warns when template used in attribute context.
790+
*
791+
* @ticket 60229
792+
*
793+
* @covers ::bind
794+
*
795+
* @expectedIncorrectUsage WP_HTML_Template::bind
796+
*/
797+
public function test_bind_warns_on_template_in_attribute_context() {
798+
$template = T::from( '<meta content="</%html>">' );
799+
$template->bind( array( 'html' => T::from( '<b>nested</b>' ) ) );
800+
}
801+
758802
/**
759803
* Verifies that get_placeholders returns placeholder metadata.
760804
*

0 commit comments

Comments
 (0)