Make WordPress Core

Changeset 56072


Ignore:
Timestamp:
06/27/2023 04:26:15 PM (8 months ago)
Author:
SergeyBiryukov
Message:

Media: Only show “Copy” and “Download” actions when an attachment URL is available.

Includes unit tests to verify the logic for displaying row actions in the Media Library in certain scenarios, e.g. with and without the “Trash” or “Unattached” filter.

Follow-up to [55949].

Props costdev, kebbet, mukesh27, oglekler.
Fixes #57893.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-wp-media-list-table.php

    r56023 r56072  
    817817            }
    818818
    819             $actions['copy'] = sprintf(
    820                 '<span class="copy-to-clipboard-container"><button type="button" class="button-link copy-attachment-url media-library" data-clipboard-text="%s" aria-label="%s">%s</button><span class="success hidden" aria-hidden="true">%s</span></span>',
     819            if ( $attachment_url ) {
     820                $actions['copy'] = sprintf(
     821                    '<span class="copy-to-clipboard-container"><button type="button" class="button-link copy-attachment-url media-library" data-clipboard-text="%s" aria-label="%s">%s</button><span class="success hidden" aria-hidden="true">%s</span></span>',
     822                    esc_url( $attachment_url ),
     823                    /* translators: %s: Attachment title. */
     824                    esc_attr( sprintf( __( 'Copy &#8220;%s&#8221; URL to clipboard' ), $att_title ) ),
     825                    __( 'Copy URL' ),
     826                    __( 'Copied!' )
     827                );
     828            }
     829        }
     830
     831        if ( $attachment_url ) {
     832            $actions['download'] = sprintf(
     833                '<a href="%s" aria-label="%s" download>%s</a>',
    821834                esc_url( $attachment_url ),
    822835                /* translators: %s: Attachment title. */
    823                 esc_attr( sprintf( __( 'Copy &#8220;%s&#8221; URL to clipboard' ), $att_title ) ),
    824                 __( 'Copy URL' ),
    825                 __( 'Copied!' )
     836                esc_attr( sprintf( __( 'Download &#8220;%s&#8221;' ), $att_title ) ),
     837                __( 'Download file' )
    826838            );
    827839        }
    828 
    829         $actions['download'] = sprintf(
    830             '<a href="%s" aria-label="%s" download>%s</a>',
    831             esc_url( $attachment_url ),
    832             /* translators: %s: Attachment title. */
    833             esc_attr( sprintf( __( 'Download &#8220;%s&#8221;' ), $att_title ) ),
    834             __( 'Download file' )
    835         );
    836840
    837841        if ( $this->detached && current_user_can( 'edit_post', $post->ID ) ) {
  • trunk/tests/phpunit/tests/admin/wpMediaListTable.php

    r53791 r56072  
    55 */
    66class Tests_Admin_wpMediaListTable extends WP_UnitTestCase {
     7    /**
     8     * A list table for testing.
     9     *
     10     * @var WP_Media_List_Table
     11     */
     12    protected static $list_table;
     13
     14    /**
     15     * A reflection of the `$is_trash` property.
     16     *
     17     * @var ReflectionProperty
     18     */
     19    protected static $is_trash;
     20
     21    /**
     22     * The original value of the `$is_trash` property.
     23     *
     24     * @var bool|null
     25     */
     26    protected static $is_trash_original;
     27
     28    /**
     29     * A reflection of the `$detached` property.
     30     *
     31     * @var ReflectionProperty
     32     */
     33    protected static $detached;
     34
     35    /**
     36     * The original value of the `$detached` property.
     37     *
     38     * @var bool|null
     39     */
     40    protected static $detached_original;
     41
     42    /**
     43     * The ID of an 'administrator' user for testing.
     44     *
     45     * @var int
     46     */
     47    protected static $admin;
     48
     49    /**
     50     * The ID of a 'subscriber' user for testing.
     51     *
     52     * @var int
     53     */
     54    protected static $subscriber;
     55
     56    /**
     57     * A post for testing.
     58     *
     59     * @var WP_Post
     60     */
     61    protected static $post;
     62
     63    /**
     64     * An attachment for testing.
     65     *
     66     * @var WP_Post
     67     */
     68    protected static $attachment;
    769
    870    public static function set_up_before_class() {
     
    1072
    1173        require_once ABSPATH . 'wp-admin/includes/class-wp-media-list-table.php';
     74
     75        self::$list_table = new WP_Media_List_Table();
     76        self::$is_trash   = new ReflectionProperty( self::$list_table, 'is_trash' );
     77        self::$detached   = new ReflectionProperty( self::$list_table, 'detached' );
     78
     79        self::$is_trash->setAccessible( true );
     80        self::$is_trash_original = self::$is_trash->getValue( self::$list_table );
     81        self::$is_trash->setAccessible( false );
     82
     83        self::$detached->setAccessible( true );
     84        self::$detached_original = self::$detached->getValue( self::$list_table );
     85        self::$detached->setAccessible( false );
     86
     87        // Create users.
     88        self::$admin      = self::factory()->user->create( array( 'role' => 'administrator' ) );
     89        self::$subscriber = self::factory()->user->create( array( 'role' => 'subscriber' ) );
     90
     91        // Create posts.
     92        self::$post       = self::factory()->post->create_and_get();
     93        self::$attachment = self::factory()->attachment->create_and_get(
     94            array(
     95                'post_name'      => 'attachment-name',
     96                'file'           => 'image.jpg',
     97                'post_mime_type' => 'image/jpeg',
     98            )
     99        );
     100    }
     101
     102    /**
     103     * Restores reflections to their original values.
     104     */
     105    public function tear_down() {
     106        self::set_is_trash( self::$is_trash_original );
     107        self::set_detached( self::$detached_original );
     108
     109        parent::tear_down();
    12110    }
    13111
     
    50148        $mock->prepare_items();
    51149    }
     150
     151    /**
     152     * Tests that `WP_Media_List_Table::_get_row_actions()` only includes an action
     153     * in certain scenarios.
     154     *
     155     * @ticket 57893
     156     *
     157     * @covers WP_Media_List_Table::_get_row_actions
     158     *
     159     * @dataProvider data_get_row_actions_should_include_action
     160     *
     161     * @param string    $action   The action that should be included.
     162     * @param string    $role     The role of the current user.
     163     * @param bool|null $trash    Whether the attachment filter is currently 'trash',
     164     *                            or `null` to leave as-is.
     165     * @param bool|null $detached Whether the attachment filter is currently 'detached',
     166     *                            or `null` to leave as-is.
     167     */
     168    public function test_get_row_actions_should_include_action( $action, $role, $trash, $detached ) {
     169        if ( 'admin' === $role ) {
     170            wp_set_current_user( self::$admin );
     171        } elseif ( 'subscriber' === $role ) {
     172            wp_set_current_user( self::$subscriber );
     173        }
     174
     175        if ( null !== $trash ) {
     176            self::set_is_trash( $trash );
     177        }
     178
     179        if ( null !== $detached ) {
     180            self::set_detached( $detached );
     181        }
     182
     183        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     184        $_get_row_actions->setAccessible( true );
     185        $actions = $_get_row_actions->invoke( self::$list_table, self::$post, 'att_title' );
     186        $_get_row_actions->setAccessible( false );
     187
     188        $this->assertIsArray( $actions, 'An array was not returned.' );
     189        $this->assertArrayHasKey( $action, $actions, "'$action' was not included in the actions." );
     190    }
     191
     192    /**
     193     * Data provider.
     194     *
     195     * @return array[]
     196     */
     197    public function data_get_row_actions_should_include_action() {
     198        return array(
     199            '"edit" while not on "trash"'  => array(
     200                'action'   => 'edit',
     201                'role'     => 'admin',
     202                'trash'    => false,
     203                'detached' => null,
     204            ),
     205            '"untrash" while on "trash"'   => array(
     206                'action'   => 'untrash',
     207                'role'     => 'admin',
     208                'trash'    => true,
     209                'detached' => null,
     210            ),
     211            '"delete" while on "trash"'    => array(
     212                'action'   => 'delete',
     213                'role'     => 'admin',
     214                'trash'    => true,
     215                'detached' => null,
     216            ),
     217            '"view" while not on "trash"'  => array(
     218                'action'   => 'view',
     219                'role'     => 'admin',
     220                'trash'    => false,
     221                'detached' => null,
     222            ),
     223            '"attach" while on "detached"' => array(
     224                'action'   => 'attach',
     225                'role'     => 'admin',
     226                'trash'    => null,
     227                'detached' => true,
     228            ),
     229        );
     230    }
     231
     232    /**
     233     * Tests that `WP_Media_List_Table::_get_row_actions()` does not include an action
     234     * in certain scenarios.
     235     *
     236     * @ticket 57893
     237     *
     238     * @covers WP_Media_List_Table::_get_row_actions
     239     *
     240     * @dataProvider data_get_row_actions_should_not_include_action
     241     *
     242     * @param string    $action   The action that should not be included.
     243     * @param string    $role     The role of the current user.
     244     * @param bool|null $trash    Whether the attachment filter is currently 'trash',
     245     *                            or `null` to leave as-is.
     246     * @param bool|null $detached Whether the attachment filter is currently 'detached',
     247     *                            or `null` to leave as-is.
     248     */
     249    public function test_get_row_actions_should_not_include_action( $action, $role, $trash, $detached ) {
     250        if ( 'admin' === $role ) {
     251            wp_set_current_user( self::$admin );
     252        } elseif ( 'subscriber' === $role ) {
     253            wp_set_current_user( self::$subscriber );
     254        }
     255
     256        if ( null !== $trash ) {
     257            self::set_is_trash( $trash );
     258        }
     259
     260        if ( null !== $detached ) {
     261            self::set_detached( $detached );
     262        }
     263
     264        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     265        $_get_row_actions->setAccessible( true );
     266        $actions = $_get_row_actions->invoke( self::$list_table, self::$post, 'att_title' );
     267        $_get_row_actions->setAccessible( false );
     268
     269        $this->assertIsArray( $actions, 'An array was not returned.' );
     270        $this->assertArrayNotHasKey( $action, $actions, "'$action' was included in the actions." );
     271    }
     272
     273    /**
     274     * Data provider.
     275     *
     276     * @return array[]
     277     */
     278    public function data_get_row_actions_should_not_include_action() {
     279        return array(
     280            '"edit" while on "trash"'               => array(
     281                'action'   => 'edit',
     282                'role'     => 'admin',
     283                'trash'    => true,
     284                'detached' => null,
     285            ),
     286            '"edit" with incorrect capabilities'    => array(
     287                'action'   => 'edit',
     288                'role'     => 'subscriber',
     289                'trash'    => false,
     290                'detached' => null,
     291            ),
     292            '"untrash" while not on "trash"'        => array(
     293                'action'   => 'untrash',
     294                'role'     => 'administrator',
     295                'trash'    => false,
     296                'detached' => null,
     297            ),
     298            '"untrash" with incorrect capabilities' => array(
     299                'action'   => 'untrash',
     300                'role'     => 'subscriber',
     301                'trash'    => true,
     302                'detached' => null,
     303            ),
     304            '"trash" while not on "trash"'          => array(
     305                'action'   => 'trash',
     306                'role'     => 'administrator',
     307                'trash'    => false,
     308                'detached' => null,
     309            ),
     310            '"trash" with incorrect capabilities'   => array(
     311                'action'   => 'trash',
     312                'role'     => 'subscriber',
     313                'trash'    => true,
     314                'detached' => null,
     315            ),
     316            '"view" while on "trash"'               => array(
     317                'action'   => 'view',
     318                'role'     => 'administrator',
     319                'trash'    => true,
     320                'detached' => null,
     321            ),
     322            '"attach" with incorrect capabilities'  => array(
     323                'action'   => 'attach',
     324                'role'     => 'subscriber',
     325                'trash'    => null,
     326                'detached' => true,
     327            ),
     328            '"attach" when not on "detached"'       => array(
     329                'action'   => 'attach',
     330                'role'     => 'administrator',
     331                'trash'    => null,
     332                'detached' => false,
     333            ),
     334            '"copy" when on "trash"'                => array(
     335                'action'   => 'copy',
     336                'role'     => 'administrator',
     337                'trash'    => true,
     338                'detached' => null,
     339            ),
     340        );
     341    }
     342
     343    /**
     344     * Tests that `WP_Media_List_Table::_get_row_actions()` does not include the 'view' action
     345     * when a permalink is not available.
     346     *
     347     * @ticket 57893
     348     *
     349     * @covers WP_Media_List_Table::_get_row_actions
     350     */
     351    public function test_get_row_actions_should_not_include_view_without_a_permalink() {
     352        self::set_is_trash( false );
     353
     354        // Ensure the permalink is `false`.
     355        add_filter( 'post_link', '__return_false', 10, 0 );
     356
     357        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     358        $_get_row_actions->setAccessible( true );
     359        $actions = $_get_row_actions->invoke( self::$list_table, self::$post, 'att_title' );
     360        $_get_row_actions->setAccessible( false );
     361
     362        $this->assertIsArray( $actions, 'An array was not returned.' );
     363        $this->assertArrayNotHasKey( 'view', $actions, '"view" was included in the actions.' );
     364    }
     365
     366    /**
     367     * Tests that `WP_Media_List_Table::_get_row_actions()` includes the 'copy' action.
     368     *
     369     * @ticket 57893
     370     *
     371     * @covers WP_Media_List_Table::_get_row_actions
     372     */
     373    public function test_get_row_actions_should_include_copy() {
     374        self::set_is_trash( false );
     375
     376        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     377        $_get_row_actions->setAccessible( true );
     378        $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
     379        $_get_row_actions->setAccessible( false );
     380
     381        $this->assertIsArray( $actions, 'An array was not returned.' );
     382        $this->assertArrayHasKey( 'copy', $actions, '"copy" was not included in the actions.' );
     383    }
     384
     385    /**
     386     * Tests that `WP_Media_List_Table::_get_row_actions()` does not include the 'copy' action
     387     * when an attachment URL is not available.
     388     *
     389     * @ticket 57893
     390     *
     391     * @covers WP_Media_List_Table::_get_row_actions
     392     */
     393    public function test_get_row_actions_should_not_include_copy_without_an_attachment_url() {
     394        self::set_is_trash( false );
     395
     396        // Ensure the attachment URL is `false`.
     397        add_filter( 'wp_get_attachment_url', '__return_false', 10, 0 );
     398
     399        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     400        $_get_row_actions->setAccessible( true );
     401        $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
     402        $_get_row_actions->setAccessible( false );
     403
     404        $this->assertIsArray( $actions, 'An array was not returned.' );
     405        $this->assertArrayNotHasKey( 'copy', $actions, '"copy" was included in the actions.' );
     406    }
     407
     408    /**
     409     * Tests that `WP_Media_List_Table::_get_row_actions()` includes the 'download' action.
     410     *
     411     * @ticket 57893
     412     *
     413     * @covers WP_Media_List_Table::_get_row_actions
     414     */
     415    public function test_get_row_actions_should_include_download() {
     416        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     417        $_get_row_actions->setAccessible( true );
     418        $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
     419        $_get_row_actions->setAccessible( false );
     420
     421        $this->assertIsArray( $actions, 'An array was not returned.' );
     422        $this->assertArrayHasKey( 'download', $actions, '"download" was not included in the actions.' );
     423    }
     424
     425    /**
     426     * Tests that `WP_Media_List_Table::_get_row_actions()` does not include the 'download' action
     427     * when an attachment URL is not available.
     428     *
     429     * @ticket 57893
     430     *
     431     * @covers WP_Media_List_Table::_get_row_actions
     432     */
     433    public function test_get_row_actions_should_not_include_download_without_an_attachment_url() {
     434        // Ensure the attachment URL is `false`.
     435        add_filter( 'wp_get_attachment_url', '__return_false', 10, 0 );
     436
     437        $_get_row_actions = new ReflectionMethod( self::$list_table, '_get_row_actions' );
     438        $_get_row_actions->setAccessible( true );
     439        $actions = $_get_row_actions->invoke( self::$list_table, self::$attachment, 'att_title' );
     440        $_get_row_actions->setAccessible( false );
     441
     442        $this->assertIsArray( $actions, 'An array was not returned.' );
     443        $this->assertArrayNotHasKey( 'download', $actions, '"download" was included in the actions.' );
     444    }
     445
     446    /**
     447     * Sets the `$is_trash` property.
     448     *
     449     * Helper method.
     450     *
     451     * @param bool $is_trash Whether the attachment filter is currently 'trash'.
     452     */
     453    private static function set_is_trash( $is_trash ) {
     454        self::$is_trash->setAccessible( true );
     455        self::$is_trash->setValue( self::$list_table, $is_trash );
     456        self::$is_trash->setAccessible( false );
     457    }
     458
     459    /**
     460     * Sets the `$detached` property.
     461     *
     462     * Helper method.
     463     *
     464     * @param bool $detached Whether the attachment filter is currently 'detached'.
     465     */
     466    private static function set_detached( $detached ) {
     467        self::$detached->setAccessible( true );
     468        self::$detached->setValue( self::$list_table, $detached );
     469        self::$detached->setAccessible( false );
     470    }
    52471}
Note: See TracChangeset for help on using the changeset viewer.