@@ -330,4 +330,194 @@ public function test_get_views_should_return_views_by_default() {
330330
331331 $ this ->assertSame ( $ expected , $ actual );
332332 }
333+
334+ /**
335+ * Renders the title column for a post in a given list view mode.
336+ *
337+ * @param WP_Post $post The post to render.
338+ * @param string $mode_value The list view mode ('list' or 'excerpt').
339+ * @return string The rendered output.
340+ */
341+ protected function render_column_title ( $ post , $ mode_value ) {
342+ global $ mode ;
343+ $ mode = $ mode_value ;
344+
345+ $ table = _get_list_table ( 'WP_Posts_List_Table ' , array ( 'screen ' => 'edit-post ' ) );
346+ $ table ->set_hierarchical_display ( false );
347+
348+ ob_start ();
349+ $ table ->column_title ( $ post );
350+ return ob_get_clean ();
351+ }
352+
353+ /**
354+ * @ticket 65022
355+ *
356+ * @dataProvider data_no_title_excerpt_visibility
357+ *
358+ * @covers WP_Posts_List_Table::column_title
359+ * @covers WP_Posts_List_Table::get_no_title_excerpt
360+ *
361+ * @param string $mode_value The list view mode ('list' or 'excerpt').
362+ * @param array $post_args Overrides for the post to create.
363+ * @param bool $should_show Whether the trimmed excerpt should be shown.
364+ */
365+ public function test_no_title_excerpt_visibility ( $ mode_value , $ post_args , $ should_show ) {
366+ wp_set_current_user ( self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) ) );
367+
368+ $ post = self ::factory ()->post ->create_and_get (
369+ array_merge (
370+ array (
371+ 'post_type ' => 'post ' ,
372+ 'post_status ' => 'publish ' ,
373+ 'post_title ' => '' ,
374+ 'post_excerpt ' => 'Alpha beta gamma delta epsilon. ' ,
375+ ),
376+ $ post_args
377+ )
378+ );
379+
380+ $ output = $ this ->render_column_title ( $ post , $ mode_value );
381+
382+ if ( $ should_show ) {
383+ $ this ->assertStringContainsString ( 'class="trimmed-post-excerpt" ' , $ output );
384+ } else {
385+ $ this ->assertStringNotContainsString ( 'class="trimmed-post-excerpt" ' , $ output );
386+ }
387+ }
388+
389+ /**
390+ * Data provider.
391+ *
392+ * @return array[]
393+ */
394+ public function data_no_title_excerpt_visibility () {
395+ return array (
396+ 'compact view, untitled ' => array ( 'list ' , array (), true ),
397+ 'extended view, untitled ' => array ( 'excerpt ' , array (), false ),
398+ 'compact view, has title ' => array ( 'list ' , array ( 'post_title ' => 'A real title ' ), false ),
399+ 'compact view, password ' => array ( 'list ' , array ( 'post_password ' => 'secret ' ), false ),
400+ );
401+ }
402+
403+ /**
404+ * @ticket 65022
405+ *
406+ * @covers WP_Posts_List_Table::column_title
407+ * @covers WP_Posts_List_Table::get_no_title_excerpt
408+ */
409+ public function test_no_title_excerpt_is_trimmed_to_15_words () {
410+ wp_set_current_user ( self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) ) );
411+
412+ $ words = array ();
413+ for ( $ n = 1 ; $ n <= 20 ; $ n ++ ) {
414+ $ words [] = 'word ' . $ n ;
415+ }
416+
417+ $ post = self ::factory ()->post ->create_and_get (
418+ array (
419+ 'post_title ' => '' ,
420+ 'post_excerpt ' => implode ( ' ' , $ words ),
421+ 'post_status ' => 'publish ' ,
422+ )
423+ );
424+
425+ $ output = $ this ->render_column_title ( $ post , 'list ' );
426+
427+ $ this ->assertStringContainsString ( 'word15 ' , $ output , 'The 15th word should be present. ' );
428+ $ this ->assertStringNotContainsString ( 'word16 ' , $ output , 'The 16th word should be trimmed. ' );
429+ $ this ->assertStringContainsString ( '… ' , $ output , 'The excerpt should end with an ellipsis. ' );
430+ }
431+
432+ /**
433+ * Ensures excerpt output is escaped, including markup reintroduced by the `wp_trim_words` filter.
434+ *
435+ * @ticket 65022
436+ *
437+ * @covers WP_Posts_List_Table::column_title
438+ * @covers WP_Posts_List_Table::get_no_title_excerpt
439+ */
440+ public function test_no_title_excerpt_is_escaped () {
441+ wp_set_current_user ( self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) ) );
442+
443+ $ post = self ::factory ()->post ->create_and_get (
444+ array (
445+ 'post_title ' => '' ,
446+ 'post_excerpt ' => 'Some excerpt text. ' ,
447+ 'post_status ' => 'publish ' ,
448+ )
449+ );
450+
451+ add_filter ( 'wp_trim_words ' , array ( $ this , 'filter_wp_trim_words_return_markup ' ) );
452+ $ output = $ this ->render_column_title ( $ post , 'list ' );
453+ remove_filter ( 'wp_trim_words ' , array ( $ this , 'filter_wp_trim_words_return_markup ' ) );
454+
455+ $ this ->assertStringContainsString ( '<script>alert(1)</script> ' , $ output , 'The markup should be escaped. ' );
456+ $ this ->assertStringNotContainsString ( '<script>alert(1)</script> ' , $ output , 'Raw markup should not be output. ' );
457+ }
458+
459+ /**
460+ * Filter callback returning markup for the `wp_trim_words` filter.
461+ *
462+ * @return string
463+ */
464+ public function filter_wp_trim_words_return_markup () {
465+ return '<script>alert(1)</script> ' ;
466+ }
467+
468+ /**
469+ * @ticket 65022
470+ *
471+ * @covers WP_Posts_List_Table::get_no_title_excerpt
472+ */
473+ public function test_no_title_excerpt_hidden_when_user_cannot_read_post () {
474+ $ author = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
475+
476+ $ post = self ::factory ()->post ->create_and_get (
477+ array (
478+ 'post_author ' => $ author ,
479+ 'post_title ' => '' ,
480+ 'post_excerpt ' => 'Private draft excerpt. ' ,
481+ 'post_status ' => 'draft ' ,
482+ )
483+ );
484+
485+ // A subscriber cannot read another user's draft.
486+ wp_set_current_user ( self ::factory ()->user ->create ( array ( 'role ' => 'subscriber ' ) ) );
487+
488+ $ output = $ this ->render_column_title ( $ post , 'list ' );
489+
490+ $ this ->assertStringNotContainsString ( 'class="trimmed-post-excerpt" ' , $ output );
491+ }
492+
493+ /**
494+ * @ticket 65022
495+ *
496+ * @covers WP_Posts_List_Table::column_cb
497+ * @covers WP_Posts_List_Table::get_no_title_excerpt
498+ */
499+ public function test_checkbox_label_includes_no_title_excerpt () {
500+ wp_set_current_user ( self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) ) );
501+
502+ global $ mode ;
503+ $ mode = 'list ' ;
504+
505+ $ post = self ::factory ()->post ->create_and_get (
506+ array (
507+ 'post_title ' => '' ,
508+ 'post_excerpt ' => 'Hello world example excerpt. ' ,
509+ 'post_status ' => 'publish ' ,
510+ )
511+ );
512+
513+ $ GLOBALS ['post ' ] = $ post ;
514+
515+ $ table = _get_list_table ( 'WP_Posts_List_Table ' , array ( 'screen ' => 'edit-post ' ) );
516+
517+ ob_start ();
518+ $ table ->column_cb ( $ post );
519+ $ output = ob_get_clean ();
520+
521+ $ this ->assertStringContainsString ( 'Select (no title) Hello world example excerpt. ' , $ output );
522+ }
333523}
0 commit comments