Skip to content

Commit fb7a7b8

Browse files
authored
Update hashtag escaping to use slugs and improve tests (#2352)
1 parent 9eac43d commit fb7a7b8

File tree

7 files changed

+132
-8
lines changed

7 files changed

+132
-8
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
Improved hashtag encoding for consistent formatting.

includes/class-shortcodes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static function hashtags() {
6262
$hash_tags[] = \sprintf(
6363
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
6464
\esc_url( \get_tag_link( $tag ) ),
65-
esc_hashtag( $tag->name )
65+
esc_hashtag( $tag->slug )
6666
);
6767
}
6868

includes/functions.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,12 @@ function snake_to_camel_case( $input ) {
266266
function esc_hashtag( $input ) {
267267

268268
$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
269-
// Remove all characters that are not letters, numbers, or underscores.
270-
$hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
269+
// Remove all characters that are not letters, numbers, or hyphens.
270+
$hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}-]+/u', '-', $hashtag );
271271

272-
// Capitalize every letter that is preceded by an underscore.
272+
// Capitalize every letter that is preceded by a hyphen.
273273
$hashtag = preg_replace_callback(
274-
'/_(.)/',
274+
'/-+(.)/',
275275
function ( $matches ) {
276276
return strtoupper( $matches[1] );
277277
},
@@ -280,6 +280,7 @@ function ( $matches ) {
280280

281281
// Add a hashtag to the beginning of the string.
282282
$hashtag = ltrim( $hashtag, '#' );
283+
$hashtag = trim( $hashtag, '-' );
283284
$hashtag = '#' . $hashtag;
284285

285286
/**

includes/model/class-blog.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ public function get_tag() {
510510
$hashtags[] = array(
511511
'type' => 'Hashtag',
512512
'href' => \get_tag_link( $tag->term_id ),
513-
'name' => esc_hashtag( $tag->name ),
513+
'name' => esc_hashtag( $tag->slug ),
514514
);
515515
}
516516

includes/rest/class-collections-controller.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public function get_tags( $request, $user_id ) {
140140
$response['items'][] = array(
141141
'type' => 'Hashtag',
142142
'href' => \esc_url( \get_tag_link( $tag ) ),
143-
'name' => esc_hashtag( $tag->name ),
143+
'name' => esc_hashtag( $tag->slug ),
144144
);
145145
}
146146

includes/transformer/class-post.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ protected function get_tag() {
443443
$tags[] = array(
444444
'type' => 'Hashtag',
445445
'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
446-
'name' => esc_hashtag( $post_tag->name ),
446+
'name' => esc_hashtag( $post_tag->slug ),
447447
);
448448
}
449449
}

tests/phpunit/tests/includes/class-test-functions.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,4 +1406,123 @@ public function camel_snake_case_provider() {
14061406
public function test_camel_to_snake_case( $original, $expected ) {
14071407
$this->assertSame( $expected, \Activitypub\camel_to_snake_case( $original ) );
14081408
}
1409+
1410+
/**
1411+
* Data provider for esc_hashtag tests.
1412+
*
1413+
* @return array Test cases with input and expected output.
1414+
*/
1415+
public function esc_hashtag_provider() {
1416+
return array(
1417+
'simple_word' => array( 'test', '#test' ),
1418+
'word_with_spaces' => array( 'test tag', '#testTag' ),
1419+
'multiple_spaces' => array( 'test multiple spaces', '#testMultipleSpaces' ),
1420+
'with_special_chars' => array( 'test@tag!', '#testTag' ),
1421+
'with_underscores' => array( 'test_tag', '#testTag' ),
1422+
'with_leading_hashtag' => array( '#test', '#Test' ),
1423+
'with_multiple_hashtags' => array( '##test', '#Test' ),
1424+
'with_leading_hyphen' => array( '-test', '#Test' ),
1425+
'with_trailing_hyphen' => array( 'test-', '#test' ),
1426+
'mixed_case' => array( 'TestTag', '#TestTag' ),
1427+
'with_numbers' => array( 'test123', '#test123' ),
1428+
'with_unicode' => array( 'tëst', '#tëst' ),
1429+
'with_unicode_spaces' => array( 'tëst tàg', '#tëstTàg' ),
1430+
'german_umlauts' => array( 'über straße', '#überStraße' ),
1431+
'japanese_characters' => array( 'テスト', '#テスト' ),
1432+
'arabic_characters' => array( 'اختبار', '#اختبار' ),
1433+
'cyrillic_characters' => array( 'тест', '#тест' ),
1434+
'empty_string' => array( '', '#' ),
1435+
'only_spaces' => array( ' ', '#' ),
1436+
'only_special_chars' => array( '@!#$%', '#' ),
1437+
'hyphenated_words' => array( 'foo-bar-baz', '#fooBarBaz' ),
1438+
'quotes' => array( "test'tag", '#testTag' ),
1439+
'double_quotes' => array( 'test"tag', '#testTag' ),
1440+
'ampersand' => array( 'test&tag', '#testTag' ),
1441+
'html_entities' => array( 'test&amp;tag', '#testTag' ),
1442+
'leading_trailing_spaces' => array( ' test ', '#Test' ),
1443+
'multiple_hyphens' => array( 'test--tag', '#testTag' ),
1444+
'camelCase_preservation' => array( 'testTag', '#testTag' ),
1445+
'with_dots' => array( 'test.tag', '#testTag' ),
1446+
'with_commas' => array( 'test,tag', '#testTag' ),
1447+
'with_semicolons' => array( 'test;tag', '#testTag' ),
1448+
'with_slashes' => array( 'test/tag', '#testTag' ),
1449+
'with_backslashes' => array( 'test\\tag', '#testTag' ),
1450+
'with_parentheses' => array( 'test(tag)', '#testTag' ),
1451+
'with_brackets' => array( 'test[tag]', '#testTag' ),
1452+
'with_braces' => array( 'test{tag}', '#testTag' ),
1453+
'emoji_mixed' => array( 'test 😀 tag', '#testTag' ),
1454+
'chinese_characters' => array( '测试 标签', '#测试标签' ),
1455+
'korean_characters' => array( '테스트 태그', '#테스트태그' ),
1456+
'greek_characters' => array( 'δοκιμή', '#δοκιμή' ),
1457+
'hebrew_characters' => array( 'בדיקה', '#בדיקה' ),
1458+
'thai_characters' => array( 'ทดสอบ', '#ทดสอบ' ),
1459+
);
1460+
}
1461+
1462+
/**
1463+
* Test esc_hashtag function.
1464+
*
1465+
* @dataProvider esc_hashtag_provider
1466+
* @covers \Activitypub\esc_hashtag
1467+
*
1468+
* @param string $input The input string.
1469+
* @param string $expected The expected hashtag output.
1470+
*/
1471+
public function test_esc_hashtag( $input, $expected ) {
1472+
$result = \Activitypub\esc_hashtag( $input );
1473+
$this->assertSame( $expected, $result );
1474+
}
1475+
1476+
/**
1477+
* Test esc_hashtag filter hook.
1478+
*
1479+
* @covers \Activitypub\esc_hashtag
1480+
*/
1481+
public function test_esc_hashtag_filter() {
1482+
$filter_callback = function ( $hashtag, $input ) {
1483+
if ( 'custom' === $input ) {
1484+
return '#CustomTag';
1485+
}
1486+
return $hashtag;
1487+
};
1488+
1489+
\add_filter( 'activitypub_esc_hashtag', $filter_callback, 10, 2 );
1490+
1491+
$result = \Activitypub\esc_hashtag( 'custom' );
1492+
$this->assertSame( '#CustomTag', $result );
1493+
1494+
\remove_filter( 'activitypub_esc_hashtag', $filter_callback, 10 );
1495+
}
1496+
1497+
/**
1498+
* Test esc_hashtag with HTML special characters.
1499+
*
1500+
* @covers \Activitypub\esc_hashtag
1501+
*/
1502+
public function test_esc_hashtag_html_escaping() {
1503+
$result = \Activitypub\esc_hashtag( '<script>alert("xss")</script>' );
1504+
$this->assertStringNotContainsString( '<script>', $result );
1505+
$this->assertStringNotContainsString( 'alert', $result );
1506+
// The result should be HTML-escaped.
1507+
$this->assertStringStartsWith( '#', $result );
1508+
}
1509+
1510+
/**
1511+
* Test esc_hashtag with quoted strings.
1512+
*
1513+
* @covers \Activitypub\esc_hashtag
1514+
*/
1515+
public function test_esc_hashtag_with_quotes() {
1516+
// Test single quotes.
1517+
$result = \Activitypub\esc_hashtag( "test's tag" );
1518+
$this->assertSame( '#testSTag', $result );
1519+
1520+
// Test double quotes.
1521+
$result = \Activitypub\esc_hashtag( 'test"s tag' );
1522+
$this->assertSame( '#testSTag', $result );
1523+
1524+
// Test HTML entities for quotes.
1525+
$result = \Activitypub\esc_hashtag( 'test&#039;s tag' );
1526+
$this->assertSame( '#testSTag', $result );
1527+
}
14091528
}

0 commit comments

Comments
 (0)