Skip to content

Commit ca64484

Browse files
committed
HTML API: Respect enqueued updates in get_attribute_names_with_prefix().
Trac ticket: Core-64567. Previously, `get_attribute_names_with_prefix()` was overlooking enqueued attribute and class name updates which occurred before `get_updated_html()` had been called. This resulted in reporting stale data which might overlook attributes which were added, and might report attributes which were removed. In this patch, the method now examines the enqueued updates to determine if any of them introduce or remove attributes. It also examines enqueued class updates to ensure that if the `class` attribute would be added or removed because of them that it will also be properly reported. Developed in: WordPress#12757 Discussed in: https://core.trac.wordpress.org/ticket/64567 Follow-up to [55203]. Props anupkankale, irozum, itzmekhokan, luisherranz, motylanogha, phpbits, sachinrajcp123, whaze. See #64567. git-svn-id: https://develop.svn.wordpress.org/trunk@62960 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 91138e1 commit ca64484

2 files changed

Lines changed: 244 additions & 4 deletions

File tree

‎src/wp-includes/html-api/class-wp-html-tag-processor.php‎

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ class WP_HTML_Tag_Processor {
753753
* );
754754
*
755755
* @since 6.2.0
756-
* @var bool[]
756+
* @var array<non-empty-string, self::ADD_CLASS|self::REMOVE_CLASS>
757757
*/
758758
private $classname_updates = array();
759759

@@ -810,7 +810,7 @@ class WP_HTML_Tag_Processor {
810810
* );
811811
*
812812
* @since 6.2.0
813-
* @var WP_HTML_Text_Replacement[]
813+
* @var array<int|string, WP_HTML_Text_Replacement>
814814
*/
815815
protected $lexical_updates = array();
816816

@@ -2970,13 +2970,45 @@ public function get_attribute_names_with_prefix( $prefix ): ?array {
29702970

29712971
$comparable = strtolower( $prefix );
29722972

2973+
/*
2974+
* For the `class` attribute, ensure that enqueued class changes from
2975+
* `add_class` and `remove_class` are flushed into attribute updates.
2976+
*/
2977+
$has_class = isset( $this->attributes['class'] );
2978+
if ( '' === $comparable || str_starts_with( 'class', $comparable ) ) {
2979+
foreach ( $this->classname_updates as $update ) {
2980+
if (
2981+
( $has_class && self::REMOVE_CLASS === $update ) ||
2982+
( ! $has_class && self::ADD_CLASS === $update )
2983+
) {
2984+
$this->class_name_updates_to_attributes_updates();
2985+
break;
2986+
}
2987+
}
2988+
}
2989+
2990+
$additions = array();
2991+
$removals = array();
2992+
foreach ( $this->lexical_updates as $update_name => $update ) {
2993+
if ( is_int( $update_name ) || 'modifiable text' === $update_name ) {
2994+
continue;
2995+
}
2996+
2997+
if ( '' === $update->text ) {
2998+
$removals[ $update_name ] = true;
2999+
} elseif ( ! isset( $this->attributes[ $update_name ] ) && str_starts_with( $update_name, $comparable ) ) {
3000+
$additions[] = $update_name;
3001+
}
3002+
}
3003+
29733004
$matches = array();
29743005
foreach ( array_keys( $this->attributes ) as $attr_name ) {
2975-
if ( str_starts_with( $attr_name, $comparable ) ) {
3006+
if ( str_starts_with( $attr_name, $comparable ) && ! isset( $removals[ $attr_name ] ) ) {
29763007
$matches[] = $attr_name;
29773008
}
29783009
}
2979-
return $matches;
3010+
3011+
return empty( $additions ) ? $matches : array_merge( $additions, $matches );
29803012
}
29813013

29823014
/**

‎tests/phpunit/tests/html-api/wpHtmlTagProcessor.php‎

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,214 @@ public function test_get_attribute_names_with_prefix_returns_attribute_added_by_
464464
);
465465
}
466466

467+
/**
468+
* Ensures that a new attribute added via set_attribute() is reported by
469+
* get_attribute_names_with_prefix() immediately after being added.
470+
*
471+
* @ticket 64567
472+
*
473+
* @covers WP_HTML_Tag_Processor::get_attribute_names_with_prefix
474+
*/
475+
public function test_get_attribute_names_with_prefix_immediately_reflects_new_attributes() {
476+
$processor = new WP_HTML_Tag_Processor( '<div existing>' );
477+
$processor->next_tag();
478+
479+
$this->assertSame(
480+
array( 'existing' ),
481+
$processor->get_attribute_names_with_prefix( '' ),
482+
'Failed to report existing attribute names: check test setup.'
483+
);
484+
485+
$processor->set_attribute( 'new', true );
486+
487+
$this->assertSame(
488+
array( 'new', 'existing' ),
489+
$processor->get_attribute_names_with_prefix( '' ),
490+
'Failed to report newly-added attribute.'
491+
);
492+
493+
$this->assertSame(
494+
array( 'existing' ),
495+
$processor->get_attribute_names_with_prefix( 'exist' ),
496+
'Should have only reported existing attribute matching given prefix.'
497+
);
498+
}
499+
500+
/**
501+
* Ensures that changes enqueued for modifiable text do not get reported as added attributes.
502+
*
503+
* This is a fairly-specific test against an internal implementation detail, but is worth
504+
* adding to catch potential regressions since modifiable text updates share a namespace with
505+
* attribute updates.
506+
*
507+
* @ticket 64567
508+
*
509+
* @covers WP_HTML_Tag_Processor::get_attribute_names_with_prefix
510+
*/
511+
public function test_get_attribute_names_with_prefix_ignores_immediately_added_modifiable_text() {
512+
$processor = new WP_HTML_Tag_Processor( '<title existing></title>' );
513+
$processor->next_tag();
514+
515+
$this->assertSame(
516+
array( 'existing' ),
517+
$processor->get_attribute_names_with_prefix( '' ),
518+
'Failed to report existing attribute names: check test setup.'
519+
);
520+
521+
$processor->set_modifiable_text( 'content!' );
522+
523+
$this->assertSame(
524+
array( 'existing' ),
525+
$processor->get_attribute_names_with_prefix( '' ),
526+
'Failed to report that the attributes were unchanged.'
527+
);
528+
}
529+
530+
/**
531+
* Ensures that an attribute removed via remove_attribute() is no longer reported
532+
* by get_attribute_names_with_prefix() immediately after being removed.
533+
*
534+
* @ticket 64567
535+
*
536+
* @covers WP_HTML_Tag_Processor::get_attribute_names_with_prefix
537+
*/
538+
public function test_get_attribute_names_with_prefix_immediately_reflects_removed_attributes() {
539+
$processor = new WP_HTML_Tag_Processor( '<div existing data-removed>' );
540+
$processor->next_tag();
541+
542+
$this->assertSame(
543+
array( 'existing', 'data-removed' ),
544+
$processor->get_attribute_names_with_prefix( '' ),
545+
'Failed to report all existing attributes: check test setup.'
546+
);
547+
548+
$processor->remove_attribute( 'data-removed' );
549+
550+
$this->assertSame(
551+
array(),
552+
$processor->get_attribute_names_with_prefix( 'data-' ),
553+
'Expected no custom data attributes after removing the only one.'
554+
);
555+
556+
$this->assertSame(
557+
array( 'existing' ),
558+
$processor->get_attribute_names_with_prefix( '' ),
559+
'Expected to report only the attribute which wasn’t removed.'
560+
);
561+
}
562+
563+
/**
564+
* Ensures that when the `class` attribute is newly added via add_class(), that
565+
* it’s reported by get_attribute_names_with_prefix() immediately after being added.
566+
*
567+
* @ticket 64567
568+
*
569+
* @covers WP_HTML_Tag_Processor::get_attribute_names_with_prefix
570+
*/
571+
public function test_get_attribute_names_with_prefix_immediately_reflects_class_after_adding_classes() {
572+
$processor = new WP_HTML_Tag_Processor( '<div existing>' );
573+
$processor->next_tag();
574+
575+
$this->assertSame(
576+
array( 'existing' ),
577+
$processor->get_attribute_names_with_prefix( '' ),
578+
'Expected to only report the existing attribute: check test setup.'
579+
);
580+
581+
$processor->add_class( 'added' );
582+
583+
$this->assertSame(
584+
array( 'class' ),
585+
$processor->get_attribute_names_with_prefix( 'class' ),
586+
'Failed to report the newly-added `class` attribute.'
587+
);
588+
}
589+
590+
/**
591+
* Ensures that when the `class` attribute is emptied via remove_class(), that
592+
* it’s reported by get_attribute_names_with_prefix() immediately after being added.
593+
*
594+
* @ticket 64567
595+
*
596+
* @covers WP_HTML_Tag_Processor::get_attribute_names_with_prefix
597+
*/
598+
public function test_get_attribute_names_with_prefix_immediately_reflects_class_after_removing_all_classes() {
599+
$processor = new WP_HTML_Tag_Processor( '<div class="red green" existing class class=duplicate>' );
600+
$processor->next_tag();
601+
602+
$this->assertSame(
603+
array( 'class', 'existing' ),
604+
$processor->get_attribute_names_with_prefix( '' ),
605+
'Expected to find proper existing attributes: check test setup.'
606+
);
607+
608+
$processor->remove_class( 'red' );
609+
610+
$this->assertSame(
611+
array( 'class', 'existing' ),
612+
$processor->get_attribute_names_with_prefix( '' ),
613+
'Should have the same attributes after removing one of two classes.'
614+
);
615+
616+
$processor->remove_class( 'green' );
617+
618+
$this->assertSame(
619+
array( 'existing' ),
620+
$processor->get_attribute_names_with_prefix( '' ),
621+
'Should have removed the `class` attribute after removing all class names.'
622+
);
623+
}
624+
625+
/**
626+
* Ensures get_attribute_names_with_prefix() agrees with get_attribute()
627+
* after pending updates, returning each name once with no stale entries.
628+
*
629+
* @ticket 64567
630+
*
631+
* @covers WP_HTML_Tag_Processor::get_attribute_names_with_prefix
632+
*/
633+
public function test_get_attribute_names_with_prefix_immediately_agrees_with_get_attribute_after_updates() {
634+
$processor = new WP_HTML_Tag_Processor( '<div data-keep="1" data-drop="2">Test</div>' );
635+
$processor->next_tag();
636+
637+
$this->assertSame(
638+
array( 'data-keep', 'data-drop' ),
639+
$processor->get_attribute_names_with_prefix( '' ),
640+
'Failed to find expected existing attributes: check test setup.'
641+
);
642+
643+
$this->assertSame(
644+
'1',
645+
$processor->get_attribute( 'data-keep' ),
646+
'Failed to find expected existing `data-keep` attribute value: check test setup.'
647+
);
648+
649+
$this->assertSame(
650+
'2',
651+
$processor->get_attribute( 'data-drop' ),
652+
'Failed to find expected existing `data-drop` attribute value: check test setup.'
653+
);
654+
655+
$processor->set_attribute( 'data-keep', 'updated' );
656+
$processor->set_attribute( 'data-add', 'new' );
657+
$processor->remove_attribute( 'data-drop' );
658+
659+
$names = $processor->get_attribute_names_with_prefix( 'data-' );
660+
661+
$this->assertSame(
662+
array( 'data-add', 'data-keep' ),
663+
$names,
664+
'Failed to report the expected attribute names after removing and adding attributes.'
665+
);
666+
667+
foreach ( $names as $name ) {
668+
$this->assertNotNull(
669+
$processor->get_attribute( $name ),
670+
"get_attribute_names_with_prefix() reported '{$name}' but get_attribute() did not agree."
671+
);
672+
}
673+
}
674+
467675
/**
468676
* @ticket 56299
469677
*

0 commit comments

Comments
 (0)