diff --git a/src/wp-admin/includes/class-wp-media-list-table.php b/src/wp-admin/includes/class-wp-media-list-table.php
index f05febb0fd..68dc685cf9 100644
--- a/src/wp-admin/includes/class-wp-media-list-table.php
+++ b/src/wp-admin/includes/class-wp-media-list-table.php
@@ -816,23 +816,27 @@ class WP_Media_List_Table extends WP_List_Table {
);
}
- $actions['copy'] = sprintf(
- '%s',
- esc_url( $attachment_url ),
- /* translators: %s: Attachment title. */
- esc_attr( sprintf( __( 'Copy “%s” URL to clipboard' ), $att_title ) ),
- __( 'Copy URL' ),
- __( 'Copied!' )
- );
+ if ( $attachment_url ) {
+ $actions['copy'] = sprintf(
+ '%s',
+ esc_url( $attachment_url ),
+ /* translators: %s: Attachment title. */
+ esc_attr( sprintf( __( 'Copy “%s” URL to clipboard' ), $att_title ) ),
+ __( 'Copy URL' ),
+ __( 'Copied!' )
+ );
+ }
}
- $actions['download'] = sprintf(
- '%s',
- esc_url( $attachment_url ),
- /* translators: %s: Attachment title. */
- esc_attr( sprintf( __( 'Download “%s”' ), $att_title ) ),
- __( 'Download file' )
- );
+ if ( $attachment_url ) {
+ $actions['download'] = sprintf(
+ '%s',
+ esc_url( $attachment_url ),
+ /* translators: %s: Attachment title. */
+ esc_attr( sprintf( __( 'Download “%s”' ), $att_title ) ),
+ __( 'Download file' )
+ );
+ }
if ( $this->detached && current_user_can( 'edit_post', $post->ID ) ) {
$actions['attach'] = sprintf(
diff --git a/tests/phpunit/tests/admin/wpMediaListTable.php b/tests/phpunit/tests/admin/wpMediaListTable.php
index a130a32506..1dc1c88af1 100644
--- a/tests/phpunit/tests/admin/wpMediaListTable.php
+++ b/tests/phpunit/tests/admin/wpMediaListTable.php
@@ -4,11 +4,109 @@
* @group admin
*/
class Tests_Admin_wpMediaListTable extends WP_UnitTestCase {
+ /**
+ * A list table for testing.
+ *
+ * @var WP_Media_List_Table
+ */
+ protected static $list_table;
+
+ /**
+ * A reflection of the `$is_trash` property.
+ *
+ * @var ReflectionProperty
+ */
+ protected static $is_trash;
+
+ /**
+ * The original value of the `$is_trash` property.
+ *
+ * @var bool|null
+ */
+ protected static $is_trash_original;
+
+ /**
+ * A reflection of the `$detached` property.
+ *
+ * @var ReflectionProperty
+ */
+ protected static $detached;
+
+ /**
+ * The original value of the `$detached` property.
+ *
+ * @var bool|null
+ */
+ protected static $detached_original;
+
+ /**
+ * The ID of an 'administrator' user for testing.
+ *
+ * @var int
+ */
+ protected static $admin;
+
+ /**
+ * The ID of a 'subscriber' user for testing.
+ *
+ * @var int
+ */
+ protected static $subscriber;
+
+ /**
+ * A post for testing.
+ *
+ * @var WP_Post
+ */
+ protected static $post;
+
+ /**
+ * An attachment for testing.
+ *
+ * @var WP_Post
+ */
+ protected static $attachment;
public static function set_up_before_class() {
parent::set_up_before_class();
require_once ABSPATH . 'wp-admin/includes/class-wp-media-list-table.php';
+
+ self::$list_table = new WP_Media_List_Table();
+ self::$is_trash = new ReflectionProperty( self::$list_table, 'is_trash' );
+ self::$detached = new ReflectionProperty( self::$list_table, 'detached' );
+
+ self::$is_trash->setAccessible( true );
+ self::$is_trash_original = self::$is_trash->getValue( self::$list_table );
+ self::$is_trash->setAccessible( false );
+
+ self::$detached->setAccessible( true );
+ self::$detached_original = self::$detached->getValue( self::$list_table );
+ self::$detached->setAccessible( false );
+
+ // Create users.
+ self::$admin = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ self::$subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) );
+
+ // Create posts.
+ self::$post = self::factory()->post->create_and_get();
+ self::$attachment = self::factory()->attachment->create_and_get(
+ array(
+ 'post_name' => 'attachment-name',
+ 'file' => 'image.jpg',
+ 'post_mime_type' => 'image/jpeg',
+ )
+ );
+ }
+
+ /**
+ * Restores reflections to their original values.
+ */
+ public function tear_down() {
+ self::set_is_trash( self::$is_trash_original );
+ self::set_detached( self::$detached_original );
+
+ parent::tear_down();
}
/**
@@ -49,4 +147,325 @@ class Tests_Admin_wpMediaListTable extends WP_UnitTestCase {
// If this test does not error out due to the PHP warning, we're good.
$mock->prepare_items();
}
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` only includes an action
+ * in certain scenarios.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ *
+ * @dataProvider data_get_row_actions_should_include_action
+ *
+ * @param string $action The action that should be included.
+ * @param string $role The role of the current user.
+ * @param bool|null $trash Whether the attachment filter is currently 'trash',
+ * or `null` to leave as-is.
+ * @param bool|null $detached Whether the attachment filter is currently 'detached',
+ * or `null` to leave as-is.
+ */
+ public function test_get_row_actions_should_include_action( $action, $role, $trash, $detached ) {
+ if ( 'admin' === $role ) {
+ wp_set_current_user( self::$admin );
+ } elseif ( 'subscriber' === $role ) {
+ wp_set_current_user( self::$subscriber );
+ }
+
+ if ( null !== $trash ) {
+ self::set_is_trash( $trash );
+ }
+
+ if ( null !== $detached ) {
+ self::set_detached( $detached );
+ }
+
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$post, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayHasKey( $action, $actions, "'$action' was not included in the actions." );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_get_row_actions_should_include_action() {
+ return array(
+ '"edit" while not on "trash"' => array(
+ 'action' => 'edit',
+ 'role' => 'admin',
+ 'trash' => false,
+ 'detached' => null,
+ ),
+ '"untrash" while on "trash"' => array(
+ 'action' => 'untrash',
+ 'role' => 'admin',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ '"delete" while on "trash"' => array(
+ 'action' => 'delete',
+ 'role' => 'admin',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ '"view" while not on "trash"' => array(
+ 'action' => 'view',
+ 'role' => 'admin',
+ 'trash' => false,
+ 'detached' => null,
+ ),
+ '"attach" while on "detached"' => array(
+ 'action' => 'attach',
+ 'role' => 'admin',
+ 'trash' => null,
+ 'detached' => true,
+ ),
+ );
+ }
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` does not include an action
+ * in certain scenarios.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ *
+ * @dataProvider data_get_row_actions_should_not_include_action
+ *
+ * @param string $action The action that should not be included.
+ * @param string $role The role of the current user.
+ * @param bool|null $trash Whether the attachment filter is currently 'trash',
+ * or `null` to leave as-is.
+ * @param bool|null $detached Whether the attachment filter is currently 'detached',
+ * or `null` to leave as-is.
+ */
+ public function test_get_row_actions_should_not_include_action( $action, $role, $trash, $detached ) {
+ if ( 'admin' === $role ) {
+ wp_set_current_user( self::$admin );
+ } elseif ( 'subscriber' === $role ) {
+ wp_set_current_user( self::$subscriber );
+ }
+
+ if ( null !== $trash ) {
+ self::set_is_trash( $trash );
+ }
+
+ if ( null !== $detached ) {
+ self::set_detached( $detached );
+ }
+
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$post, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayNotHasKey( $action, $actions, "'$action' was included in the actions." );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_get_row_actions_should_not_include_action() {
+ return array(
+ '"edit" while on "trash"' => array(
+ 'action' => 'edit',
+ 'role' => 'admin',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ '"edit" with incorrect capabilities' => array(
+ 'action' => 'edit',
+ 'role' => 'subscriber',
+ 'trash' => false,
+ 'detached' => null,
+ ),
+ '"untrash" while not on "trash"' => array(
+ 'action' => 'untrash',
+ 'role' => 'administrator',
+ 'trash' => false,
+ 'detached' => null,
+ ),
+ '"untrash" with incorrect capabilities' => array(
+ 'action' => 'untrash',
+ 'role' => 'subscriber',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ '"trash" while not on "trash"' => array(
+ 'action' => 'trash',
+ 'role' => 'administrator',
+ 'trash' => false,
+ 'detached' => null,
+ ),
+ '"trash" with incorrect capabilities' => array(
+ 'action' => 'trash',
+ 'role' => 'subscriber',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ '"view" while on "trash"' => array(
+ 'action' => 'view',
+ 'role' => 'administrator',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ '"attach" with incorrect capabilities' => array(
+ 'action' => 'attach',
+ 'role' => 'subscriber',
+ 'trash' => null,
+ 'detached' => true,
+ ),
+ '"attach" when not on "detached"' => array(
+ 'action' => 'attach',
+ 'role' => 'administrator',
+ 'trash' => null,
+ 'detached' => false,
+ ),
+ '"copy" when on "trash"' => array(
+ 'action' => 'copy',
+ 'role' => 'administrator',
+ 'trash' => true,
+ 'detached' => null,
+ ),
+ );
+ }
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` does not include the 'view' action
+ * when a permalink is not available.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ */
+ public function test_get_row_actions_should_not_include_view_without_a_permalink() {
+ self::set_is_trash( false );
+
+ // Ensure the permalink is `false`.
+ add_filter( 'post_link', '__return_false', 10, 0 );
+
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$post, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayNotHasKey( 'view', $actions, '"view" was included in the actions.' );
+ }
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` includes the 'copy' action.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ */
+ public function test_get_row_actions_should_include_copy() {
+ self::set_is_trash( false );
+
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayHasKey( 'copy', $actions, '"copy" was not included in the actions.' );
+ }
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` does not include the 'copy' action
+ * when an attachment URL is not available.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ */
+ public function test_get_row_actions_should_not_include_copy_without_an_attachment_url() {
+ self::set_is_trash( false );
+
+ // Ensure the attachment URL is `false`.
+ add_filter( 'wp_get_attachment_url', '__return_false', 10, 0 );
+
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayNotHasKey( 'copy', $actions, '"copy" was included in the actions.' );
+ }
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` includes the 'download' action.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ */
+ public function test_get_row_actions_should_include_download() {
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayHasKey( 'download', $actions, '"download" was not included in the actions.' );
+ }
+
+ /**
+ * Tests that `WP_Media_List_Table::_get_row_actions()` does not include the 'download' action
+ * when an attachment URL is not available.
+ *
+ * @ticket 57893
+ *
+ * @covers WP_Media_List_Table::_get_row_actions
+ */
+ public function test_get_row_actions_should_not_include_download_without_an_attachment_url() {
+ // Ensure the attachment URL is `false`.
+ add_filter( 'wp_get_attachment_url', '__return_false', 10, 0 );
+
+ $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
+ $_get_row_actions->setAccessible( true );
+ $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
+ $_get_row_actions->setAccessible( false );
+
+ $this->assertIsArray( $actions, 'An array was not returned.' );
+ $this->assertArrayNotHasKey( 'download', $actions, '"download" was included in the actions.' );
+ }
+
+ /**
+ * Sets the `$is_trash` property.
+ *
+ * Helper method.
+ *
+ * @param bool $is_trash Whether the attachment filter is currently 'trash'.
+ */
+ private static function set_is_trash( $is_trash ) {
+ self::$is_trash->setAccessible( true );
+ self::$is_trash->setValue( self::$list_table, $is_trash );
+ self::$is_trash->setAccessible( false );
+ }
+
+ /**
+ * Sets the `$detached` property.
+ *
+ * Helper method.
+ *
+ * @param bool $detached Whether the attachment filter is currently 'detached'.
+ */
+ private static function set_detached( $detached ) {
+ self::$detached->setAccessible( true );
+ self::$detached->setValue( self::$list_table, $detached );
+ self::$detached->setAccessible( false );
+ }
}