@@ -31,6 +31,13 @@ class Test_Attachments extends \WP_UnitTestCase {
3131 */
3232 protected static $ author_id ;
3333
34+ /**
35+ * Emoji test directory path.
36+ *
37+ * @var string
38+ */
39+ protected static $ emoji_dir ;
40+
3441 /**
3542 * Set up before class.
3643 */
@@ -51,6 +58,25 @@ public static function set_up_before_class() {
5158 'post_author ' => self ::$ author_id ,
5259 )
5360 );
61+
62+ // Create emoji test directory.
63+ $ upload_dir = \wp_upload_dir ();
64+ self ::$ emoji_dir = $ upload_dir ['basedir ' ] . Attachments::$ emoji_dir ;
65+ \wp_mkdir_p ( self ::$ emoji_dir );
66+ }
67+
68+ /**
69+ * Clean up after all tests.
70+ */
71+ public static function tear_down_after_class () {
72+ global $ wp_filesystem ;
73+ \WP_Filesystem ();
74+
75+ if ( $ wp_filesystem ->is_dir ( self ::$ emoji_dir ) ) {
76+ $ wp_filesystem ->rmdir ( self ::$ emoji_dir , true );
77+ }
78+
79+ parent ::tear_down_after_class ();
5480 }
5581
5682 /**
@@ -1325,4 +1351,85 @@ public function test_import_emoji_returns_false_on_download_failure() {
13251351
13261352 $ this ->assertFalse ( $ result );
13271353 }
1354+
1355+ /**
1356+ * Call private get_emoji_url method via reflection.
1357+ *
1358+ * @param string $emoji_url The emoji URL.
1359+ * @return string|false The local URL or false.
1360+ */
1361+ private function call_get_emoji_url ( $ emoji_url ) {
1362+ $ method = new \ReflectionMethod ( Attachments::class, 'get_emoji_url ' );
1363+ $ method ->setAccessible ( true );
1364+
1365+ return $ method ->invoke ( null , $ emoji_url );
1366+ }
1367+
1368+ /**
1369+ * Test that glob metacharacters in emoji URLs don't match unintended files.
1370+ *
1371+ * Without sanitization, glob patterns could match unintended files:
1372+ * - '[abc].*' would match files starting with a, b, or c
1373+ * - '*.*' would match all files
1374+ * - '?.*' would match any single-character filename
1375+ *
1376+ * With sanitization, these metacharacters are removed.
1377+ *
1378+ * @covers ::get_emoji_url
1379+ * @dataProvider data_glob_metacharacter_urls
1380+ */
1381+ public function test_get_emoji_url_sanitizes_glob_metacharacters ( $ malicious_filename , $ files_to_create ) {
1382+ global $ wp_filesystem ;
1383+ \WP_Filesystem ();
1384+
1385+ $ domain_dir = self ::$ emoji_dir . 'glob-test.example.com ' ;
1386+ $ wp_filesystem ->mkdir ( $ domain_dir , FS_CHMOD_DIR );
1387+
1388+ // Create files that would match the unsanitized glob pattern.
1389+ foreach ( $ files_to_create as $ filename ) {
1390+ $ wp_filesystem ->put_contents ( $ domain_dir . '/ ' . $ filename , 'test ' );
1391+ }
1392+
1393+ // URL with glob metacharacters.
1394+ $ url = 'https://glob-test.example.com/emoji/ ' . $ malicious_filename ;
1395+ $ result = $ this ->call_get_emoji_url ( $ url );
1396+
1397+ // Should NOT match any files because metacharacters are sanitized.
1398+ $ this ->assertFalse ( $ result , sprintf ( 'Glob pattern "%s" should not match existing files ' , $ malicious_filename ) );
1399+ }
1400+
1401+ /**
1402+ * Data provider for glob metacharacter tests.
1403+ *
1404+ * @return array Test cases: [malicious_filename, files_that_would_match_if_not_sanitized].
1405+ */
1406+ public function data_glob_metacharacter_urls () {
1407+ return array (
1408+ 'brackets ' => array ( '[abc].png ' , array ( 'a.png ' , 'b.png ' , 'c.png ' ) ),
1409+ 'asterisk ' => array ( 'test*.png ' , array ( 'test1.png ' , 'test2.png ' , 'testing.png ' ) ),
1410+ 'question_mark ' => array ( 'tes?.png ' , array ( 'test.png ' , 'tess.png ' , 'tesx.png ' ) ),
1411+ 'curly_braces ' => array ( '{foo,bar}.png ' , array ( 'foo.png ' , 'bar.png ' ) ),
1412+ );
1413+ }
1414+
1415+ /**
1416+ * Test that get_emoji_url finds cached files for normal URLs.
1417+ *
1418+ * @covers ::get_emoji_url
1419+ */
1420+ public function test_get_emoji_url_finds_cached_file () {
1421+ global $ wp_filesystem ;
1422+ \WP_Filesystem ();
1423+
1424+ $ domain_dir = self ::$ emoji_dir . 'cache-test.example.com ' ;
1425+ $ wp_filesystem ->mkdir ( $ domain_dir , FS_CHMOD_DIR );
1426+ $ wp_filesystem ->put_contents ( $ domain_dir . '/normal-emoji.png ' , 'test ' );
1427+
1428+ // Normal URL should find the cached file.
1429+ $ url = 'https://cache-test.example.com/emoji/normal-emoji.png ' ;
1430+ $ result = $ this ->call_get_emoji_url ( $ url );
1431+
1432+ $ this ->assertNotFalse ( $ result );
1433+ $ this ->assertStringContainsString ( 'normal-emoji.png ' , $ result );
1434+ }
13281435}
0 commit comments