Making WordPress.org

Ticket #5654: plugin-capabilities.diff

File plugin-capabilities.diff, 18.8 KB (added by dd32, 2 years ago)

WIP of finer-grained caps

  • plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php

    use WordPressdotorg\Plugin_Directory\Ema 
    1717 */
    1818class Plugin_Release_Confirmation extends Base {
    1919
    2020        public function __construct() {
    2121                register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/release-confirmation', [
    2222                        'methods'             => \WP_REST_Server::CREATABLE,
    2323                        'callback'            => [ $this, 'enable_release_confirmation' ],
    2424                        'args'                => [
    2525                                'plugin_slug' => [
    2626                                        'validate_callback' => [ $this, 'validate_plugin_slug_callback' ],
    2727                                ],
    2828                        ],
    2929                        'permission_callback' => function( $request ) {
    3030                                $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
    3131
    32                                 return current_user_can( 'plugin_admin_edit', $plugin ) && 'publish' === $plugin->post_status;
     32                                return current_user_can( 'plugin_manage_releases', $plugin );
    3333                        },
    3434                ] );
    3535
    3636                register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/release-confirmation/(?P<plugin_tag>[^/]+)', [
    3737                        'methods'             => \WP_REST_Server::READABLE, // TODO: This really should be a POST
    3838                        'callback'            => [ $this, 'confirm_release' ],
    3939                        'args'                => [
    4040                                'plugin_slug' => [
    4141                                        'validate_callback' => [ $this, 'validate_plugin_slug_callback' ],
    4242                                ],
    4343                                'plugin_tag' => [
    4444                                        'validate_callback' => [ $this, 'validate_plugin_tag_callback' ],
    4545                                ]
    4646                        ],
    4747                        'permission_callback' => function( $request ) {
    4848                                $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
    4949
    5050                                return (
    5151                                        Release_Confirmation_Shortcode::can_access() &&
    52                                         current_user_can( 'plugin_admin_edit', $plugin ) &&
    53                                         'publish' === $plugin->post_status
     52                                        current_user_can( 'plugin_manage_releases', $plugin )
    5453                                );
    5554                        },
    5655                ] );
    5756
    5857                register_rest_route( 'plugins/v1', '/release-confirmation-access', [
    5958                        'methods'             => \WP_REST_Server::READABLE,
    6059                        'callback'            => [ $this, 'send_access_email' ],
    6160                        'args'                => [
    6261                        ],
    6362                        'permission_callback' => 'is_user_logged_in',
    6463                ] );
    6564
    6665                add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 );
    6766        }
    6867
  • plugins/plugin-directory/api/routes/class-plugin-self-close.php

    use WordPressdotorg\Plugin_Directory\Too 
    1212 */
    1313class Plugin_Self_Close extends Base {
    1414
    1515        public function __construct() {
    1616                register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/self-close', [
    1717                        'methods'             => \WP_REST_Server::CREATABLE,
    1818                        'callback'            => [ $this, 'self_close' ],
    1919                        'args'                => [
    2020                                'plugin_slug' => [
    2121                                        'validate_callback' => [ $this, 'validate_plugin_slug_callback' ],
    2222                                ],
    2323                        ],
    2424                        'permission_callback' => function( $request ) {
    2525                                $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
    2626
    27                                 return current_user_can( 'plugin_admin_edit', $plugin ) && 'publish' === $plugin->post_status;
     27                                return current_user_can( 'plugin_self_close', $plugin );
    2828                        },
    2929                ] );
    3030
    3131                add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 );
    3232        }
    3333
    3434        /**
    3535         * Redirect back to the plugins page when this endpoint is accessed with an invalid nonce.
    3636         */
    3737        function override_cookie_expired_message( $result, $obj, $request ) {
    3838                if (
    3939                        is_array( $result ) && isset( $result['code'] ) &&
    4040                        preg_match( '!^/plugins/v1/plugin/([^/]+)/self-close$!', $request->get_route(), $m )
    4141                ) {
    4242                        if ( 'rest_cookie_invalid_nonce' == $result['code'] ) {
  • plugins/plugin-directory/api/routes/class-plugin-self-transfer.php

    class Plugin_Self_Transfer extends Base 
    1818                        'methods'             => \WP_REST_Server::CREATABLE,
    1919                        'callback'            => [ $this, 'self_transfer' ],
    2020                        'args'                => [
    2121                                'plugin_slug' => [
    2222                                        'validate_callback' => [ $this, 'validate_plugin_slug_callback' ],
    2323                                ],
    2424                                'new_owner' => [
    2525                                        'validate_callback' => function( $id ) {
    2626                                                return (bool) get_user_by( 'id', $id );
    2727                                        }
    2828                                ]
    2929                        ],
    3030                        'permission_callback' => function( $request ) {
    3131                                $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] );
    3232
    33                                 return
    34                                         current_user_can( 'plugin_admin_edit', $plugin ) &&
    35                                         get_current_user_id() == $plugin->post_author &&
    36                                         'publish' === $plugin->post_status;
     33                                return current_user_can( 'plugin_self_transfer', $plugin );
    3734                        },
    3835                ] );
    3936
    4037                add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 );
    4138        }
    4239
    4340        /**
    4441         * Redirect back to the plugins page when this endpoint is accessed with an invalid nonce.
    4542         */
    4643        function override_cookie_expired_message( $result, $obj, $request ) {
    4744                if (
    4845                        is_array( $result ) && isset( $result['code'] ) &&
    4946                        preg_match( '!^/plugins/v1/plugin/([^/]+)/self-transfer$!', $request->get_route(), $m )
    5047                ) {
    5148                        if ( 'rest_cookie_invalid_nonce' == $result['code'] ) {
  • plugins/plugin-directory/class-capabilities.php

    use WordPressdotorg\Plugin_Directory\Too 
    1010 */
    1111class Capabilities {
    1212
    1313        /**
    1414         * Filters a user's capabilities depending on specific context and/or privilege.
    1515         *
    1616         * @static
    1717         *
    1818         * @param array  $required_caps Returns the user's actual capabilities.
    1919         * @param string $cap           Capability name.
    2020         * @param int    $user_id       The user ID.
    2121         * @param array  $context       Adds the context to the cap. Typically the object ID.
    2222         * @return array Primitive caps.
    2323         */
    2424        public static function map_meta_cap( $required_caps, $cap, $user_id, $context ) {
    25                 $plugin_edit_cap = false;
    26                 switch ( $cap ) {
    27                         case 'plugin_admin_edit':
    28                         case 'plugin_add_committer':
    29                         case 'plugin_remove_committer':
    30                         case 'plugin_add_support_rep':
    31                         case 'plugin_remove_support_rep':
    32                                 $plugin_edit_cap = true;
    33 
    34                                 // Fall through
    35                                 // Although we no longer have a admin view, this capability is still used to determine if the current user is a committer/contributor.
    36                         case 'plugin_admin_view':
    37                                 // Committers + Contributors.
    38                                 // If no committers, post_author.
    39                                 $required_caps = array();
    40                                 $post          = get_post( $context[0] );
    41 
    42                                 if ( ! $post ) {
    43                                         $required_caps[] = 'do_not_allow';
    44                                         break;
    45                                 }
     25                $handled_caps = array(
     26                        // All these caps must pass a WP_Post context.
     27                        'plugin_admin_view',
     28                        'plugin_admin_edit',
     29                        'plugin_add_committer',
     30                        'plugin_remove_committer',
     31                        'plugin_add_support_rep',
     32                        'plugin_remove_support_rep',
     33                        'plugin_self_transfer',
     34                        'plugin_self_close',
     35                        'plugin_manage_releases',
     36                );
     37                if ( ! in_array( $cap, $handled_caps ) ) {
     38                        return $required_caps;
     39                }
    4640
    47                                 $user = new \WP_User( $user_id );
    48                                 if ( $user->has_cap( 'plugin_review' ) ) {
    49                                         $required_caps[] = 'plugin_review';
    50                                         break;
    51                                 }
     41                // Protect against a cap call without a plugin context.
     42                $post = $context ? get_post( $context[0] ) : false;
     43                if ( ! $post ) {
     44                        return array( 'do_not_allow' );
     45                }
    5246
    53                                 // Committers
    54                                 $committers = Tools::get_plugin_committers( $post->post_name );
    55                                 if ( ! $committers && 'publish' === $post->post_status ) {
    56                                         // post_author in the event no committers exist (yet?)
    57                                         $committers = array( get_user_by( 'ID', $post->post_author )->user_login );
    58                                 }
     47                // Start over, we'll specify all caps below.
     48                $required_caps = array();
    5949
    60                                 if ( in_array( $user->user_login, $committers ) ) {
    61                                         $required_caps[] = 'exist'; // All users are allowed to exist, even when they have no role.
    62                                         break;
    63                                 }
     50                // Certain actions require the plugin to be published.
     51                if (
     52                        'publish' !== $post->post_status &&
     53                        in_array(
     54                                $cap,
     55                                array(
     56                                        'plugin_self_transfer',
     57                                        'plugin_self_close',
     58                                        'plugin_manage_releases',
     59                                )
     60                        )
     61                ) {
     62                        $required_caps[] = 'do_not_allow';
     63                }
     64
     65                // If a plugin is in the Beta or Featured views, they're not able to self-manage certain things. Require reviewer.
     66                if (
     67                        in_array(
     68                                $cap,
     69                                array(
     70                                        'plugin_self_close',
     71                                        'plugin_self_transfer',
     72                                        'plugin_add_committer',
     73                                        'plugin_remove_committer',
     74                                )
     75                        ) &&
     76                        is_object_in_term( $post->ID, 'plugin_section', array( 'beta', 'featured' ) )
     77                ) {
     78                        $required_caps[] = 'plugin_review';
     79                }
    6480
    65                                 if ( ! $plugin_edit_cap ) {
    66                                         // Contributors can view, but not edit.
    67                                         $terms = get_the_terms( $post, 'plugin_contributors' );
    68                                         if ( is_array( $terms ) ) {
    69                                                 $contributors = (array) wp_list_pluck( $terms, 'name' );
    70                                                 if ( in_array( $user->user_nicename, $contributors, true ) ) {
    71                                                         $required_caps[] = 'exist'; // All users are allowed to exist, even when they have no role.
    72                                                         break;
    73                                                 }
    74                                         }
     81                // Only the Owner of a plugin is able to transfer plugins.
     82                if ( 'plugin_self_transfer' === $cap && $user_id !== $post->post_author ) {
     83                        $required_caps[] = 'do_not_allow';
     84                }
     85
     86                // Committers
     87                $committers = Tools::get_plugin_committers( $post->post_name );
     88                // If there are no committers, use the plugin author if the plugin is published.
     89                if ( ! $committers && 'publish' === $post->post_status ) {
     90                        $committers = array( get_user_by( 'ID', $post->post_author )->user_login );
     91                }
     92
     93                if ( in_array( $user->user_login, $committers ) ) {
     94                        $required_caps[] = 'exist';
     95                }
     96
     97                // Contributors can view, but not edit.
     98                if ( 'plugin_admin_view' === $cap ) {
     99                        $terms = get_the_terms( $post, 'plugin_contributors' );
     100                        if ( is_array( $terms ) ) {
     101                                $contributors = (array) wp_list_pluck( $terms, 'name' );
     102                                if ( in_array( $user->user_nicename, $contributors, true ) ) {
     103                                        $required_caps[] = 'exist';
    75104                                }
     105                        }
     106                }
     107
     108                // Allow users with review caps to access.
     109                $user = new \WP_User( $user_id );
     110                if ( $user->has_cap( 'plugin_review' ) ) {
     111                        $required_caps[] = 'plugin_review';
     112                }
    76113
    77                                 // Else;
    78                                 $required_caps[] = 'do_not_allow';
    79                                 break;
    80 
    81                         case 'plugin_transition':
    82                                 /*
    83                                  Handle the transition between
    84                                  pending -> publish
    85                                  publish -> rejected
    86                                  publish -> closed
    87                                  etc
    88                                 */
    89                                 break;
     114                // If we've not found a matching user/cap, deny.
     115                if ( ! $required_caps ) {
     116                        $required_caps[] = 'do_not_allow';
    90117                }
    91118
    92                 return $required_caps;
     119                return array_unique( $required_caps );
    93120        }
    94121
    95122        /**
    96123         * Sets up custom roles and makes them available.
    97124         *
    98125         * @static
    99126         */
    100127        public static function add_roles() {
    101128                $reviewer = array(
    102129                        'read'                    => true,
    103130                        'plugin_set_category'     => true,
    104131                        'moderate_comments'       => true,
    105132                        'plugin_edit_pending'     => true,
    106133                        'plugin_review'           => true,
    107134                        'plugin_dashboard_access' => true,
  • themes/pub/wporg-plugins/inc/template-tags.php

    function the_unconfirmed_releases_notice 
    283283        }
    284284
    285285        if ( ! $warning ) {
    286286                return;
    287287        }
    288288
    289289        printf(
    290290                '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>',
    291291                sprintf(
    292292                        __( 'This plugin has <a href="%s">a pending release that requires confirmation</a>.', 'wporg-plugins' ),
    293293                        home_url( '/developers/releases/' ) // TODO: Hardcoded URL.
    294294                )
    295295        );
    296296}
    297297
     298function the_no_self_management_notice() {
     299        $post = get_post();
     300
     301        // Check if they can access plugin management, but can't add committers.
     302        // This means the plugin has limited self-management functionalities, for security.
     303        if (
     304                current_user_can( 'plugin_admin_edit', $post ) &&
     305                ! current_user_can( 'plugin_add_committer', $post )
     306        ) {
     307                printf(
     308                        '<div class="plugin-notice notice notice-warning notice-alt"><p>%s</p></div>',
     309                        __( 'Management of this plugin has been limited for security reasons. Please contact the plugins team for assistance to add/remove committers, or to perform other actions that are unavailable.', 'wporg-plugins' )
     310                );
     311        }
     312}
     313
    298314/**
    299315 * Display the ADVANCED Zone.
    300316 */
    301317function the_plugin_advanced_zone() {
    302318        $post = get_post();
    303319
    304320        // If the post is closed, this all goes away.
    305321        if ( 'publish' !== $post->post_status ) {
    306322                return;
    307323        }
    308324
    309325        echo '<hr>';
    310326
    311327        echo '<h2>' . esc_html__( 'Advanced Options', 'wporg-plugins' ) . '</h2>';
    312328
    function the_plugin_danger_zone() { 
    395411                // Output the self close button.
    396412                the_plugin_self_close_button();
    397413        }
    398414
    399415}
    400416
    401417/**
    402418 * Displays a form for plugin committers to self-close a plugin. Permanently.
    403419 * It is disabled for plugins with 20,000+ users.
    404420 */
    405421function the_plugin_self_close_button() {
    406422        $post            = get_post();
    407423        $active_installs = (int) get_post_meta( $post->ID, 'active_installs', true );
    408424        $close_link      = false;
    409425
    410         if ( ! current_user_can( 'plugin_admin_edit', $post ) || 'publish' != $post->post_status ) {
     426        if ( ! current_user_can( 'plugin_self_close', $post ) ) {
    411427                return;
    412428        }
    413429
    414430        echo '<h4>' . esc_html__( 'Close This Plugin', 'wporg-plugins' ) . '</h4>';
    415431        echo '<p>' . esc_html__( 'This plugin is currently open. All developers have the ability to close their own plugins at any time.', 'wporg-plugins' ) . '</p>';
    416432
    417433        echo '<div class="plugin-notice notice notice-warning notice-alt"><p>';
    418434        if ( $active_installs >= 20000 ) {
    419435                // Translators: %s is the plugin team email address.
    420436                printf( __( '<strong>Notice:</strong> Due to the high volume of users for this plugin it cannot be closed without speaking directly to the plugins team. Please contact <a href="mailto:%1$s">%1$s</a> with a link to the plugin and explanation as to why it should be closed.', 'wporg-plugins' ), 'plugins@wordpress.org' );
    421437        } else {
    422438                $close_link = Template::get_self_close_link( $post );
    423439                _e( '<strong>Warning:</strong> Closing a plugin is intended to be a <em>permanent</em> action. There is no way to reopen a plugin without contacting the plugins team.', 'wporg-plugins' );
    424440        }
    425441        echo '</p></div>';
    function the_plugin_self_close_button() 
    427443        if ( $close_link ) {
    428444                echo '<form method="POST" action="' . esc_url( $close_link ) . '" onsubmit="return confirm( jQuery(this).prev(\'.notice\').text() );">';
    429445                // Translators: %s is the plugin name, as defined by the plugin itself.
    430446                echo '<p><input class="button" type="submit" value="' . esc_attr( sprintf( __( 'I understand, please close %s.', 'wporg-plugins' ), get_the_title() ) ) . '" /></p>';
    431447                echo '</form>';
    432448        }
    433449}
    434450
    435451/**
    436452 * Display a form to allow a plugin owner to transfer the ownership of a plugin to someone else.
    437453 * This does NOT remove their commit ability.
    438454 */
    439455function the_plugin_self_transfer_form() {
    440456        $post = get_post();
    441457
    442         if (
    443                 ! current_user_can( 'plugin_admin_edit', $post ) ||
    444                 'publish' != $post->post_status
    445         ) {
     458        if ( ! current_user_can( 'plugin_self_transfer', $post ) ) {
    446459                return;
    447460        }
    448461
    449462        echo '<h4>' . esc_html__( 'Transfer This Plugin', 'wporg-plugins' ) . '</h4>';
    450463
    451464        if ( get_current_user_id() != $post->post_author ) {
    452465                $owner = get_user_by( 'id', $post->post_author );
    453466                /* translators: %s: Name of plugin owner */
    454467                echo '<p>' . esc_html( sprintf(
    455468                        __( 'This plugin is currently owned by %s, they can choose to transfer ownership rights of the plugin to you.', 'wporg-plugins' ),
    456469                        $owner->display_name
    457470                ) ) . '</p>';
    458471                return;
    459472        }
    460473
    function the_plugin_self_transfer_form() 
    483496                        '<option value="%d">%s</option>' . "\n",
    484497                        esc_attr( $user->ID ),
    485498                        esc_html( $user->display_name . ' (' . $user->user_login . ')' )
    486499                );
    487500        }
    488501        echo '</select></p>';
    489502        // Translators: %s is the plugin name, as defined by the plugin itself.
    490503        echo '<p><input class="button" type="submit" value="' . esc_attr( sprintf( __( 'Please transfer %s.', 'wporg-plugins' ), get_the_title() ) ) . '" /></p>';
    491504        echo '</form>';
    492505
    493506}
    494507
    495508function the_plugin_release_confirmation_form() {
    496509        $post = get_post();
    497510
    498         if (
    499                 ! current_user_can( 'plugin_admin_edit', $post ) ||
    500                 'publish' != $post->post_status
    501         ) {
     511        if ( ! current_user_can( 'plugin_manage_releases', $post ) ) {
    502512                return;
    503513        }
    504514
    505515        $confirmations_required = $post->release_confirmation;
    506516
    507517        echo '<h4>' . esc_html__( 'Release Confirmation', 'wporg-plugins' ) . '</h4>';
    508518        if ( $confirmations_required ) {
    509519                echo '<p>' . __( 'Release confirmations for this plugin are <strong>enabled</strong>.', 'wporg-plugins' ) . '</p>';
    510520        } else {
    511521                echo '<p>' . __( 'Release confirmations for this plugin are <strong>disabled</strong>', 'wporg-plugins' ) . '</p>';
    512522        }
    513523        echo '<p>' . esc_html__( 'All future releases will require email confirmation before being made available. This increases security and ensures that plugin releases are only made when intended.', 'wporg-plugins' ) . '</p>';
    514524
    515525        if ( ! $confirmations_required && 'trunk' === $post->stable_tag ) {
    516526                echo '<div class="plugin-notice notice notice-warning notice-alt"><p>';
  • themes/pub/wporg-plugins/template-parts/section-advanced.php

     
    44 *
    55 * @link https://codex.wordpress.org/Template_Hierarchy
    66 *
    77 * @package WordPressdotorg\Plugin_Directory\Theme
    88 */
    99
    1010namespace WordPressdotorg\Plugin_Directory\Theme;
    1111
    1212use WordPressdotorg\Plugin_Directory\Template;
    1313
    1414global $post;
    1515?>
    1616
    1717<div id="admin" class="section">
    1818        <?php the_closed_plugin_notice(); ?>
     19        <?php the_no_self_management_notice(); ?>
    1920
    2021        <h2><?php esc_html_e( 'Statistics', 'wporg-plugins' ); ?></h2>
    2122
    2223        <h4><?php esc_html_e( 'Active versions', 'wporg-plugins' ); ?></h4>
    2324        <div id="plugin-version-stats" class="chart version-stats"></div>
    2425
    2526        <h4><?php esc_html_e( 'Downloads Per Day', 'wporg-plugins' ); ?></h4>
    2627        <div id="plugin-download-stats" class="chart download-stats"></div>
    2728
    2829        <h4><?php esc_html_e( 'Active Install Growth', 'wporg-plugins' ); ?></h4>
    2930        <div id="plugin-growth-stats" class="chart download-stats"></div>
    3031
    3132        <h4><?php esc_html_e( 'Downloads history', 'wporg-plugins' ); ?></h4>
    3233        <table id="plugin-download-history-stats" class="download-history-stats">
    3334                <tbody></tbody>