Ticket #5654: plugin-capabilities.diff
File plugin-capabilities.diff, 18.8 KB (added by , 3 years ago) |
---|
-
plugins/plugin-directory/api/routes/class-plugin-release-confirmation.php
use WordPressdotorg\Plugin_Directory\Ema 17 17 */ 18 18 class Plugin_Release_Confirmation extends Base { 19 19 20 20 public function __construct() { 21 21 register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/release-confirmation', [ 22 22 'methods' => \WP_REST_Server::CREATABLE, 23 23 'callback' => [ $this, 'enable_release_confirmation' ], 24 24 'args' => [ 25 25 'plugin_slug' => [ 26 26 'validate_callback' => [ $this, 'validate_plugin_slug_callback' ], 27 27 ], 28 28 ], 29 29 'permission_callback' => function( $request ) { 30 30 $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] ); 31 31 32 return current_user_can( 'plugin_ admin_edit', $plugin ) && 'publish' === $plugin->post_status;32 return current_user_can( 'plugin_manage_releases', $plugin ); 33 33 }, 34 34 ] ); 35 35 36 36 register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/release-confirmation/(?P<plugin_tag>[^/]+)', [ 37 37 'methods' => \WP_REST_Server::READABLE, // TODO: This really should be a POST 38 38 'callback' => [ $this, 'confirm_release' ], 39 39 'args' => [ 40 40 'plugin_slug' => [ 41 41 'validate_callback' => [ $this, 'validate_plugin_slug_callback' ], 42 42 ], 43 43 'plugin_tag' => [ 44 44 'validate_callback' => [ $this, 'validate_plugin_tag_callback' ], 45 45 ] 46 46 ], 47 47 'permission_callback' => function( $request ) { 48 48 $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] ); 49 49 50 50 return ( 51 51 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 ) 54 53 ); 55 54 }, 56 55 ] ); 57 56 58 57 register_rest_route( 'plugins/v1', '/release-confirmation-access', [ 59 58 'methods' => \WP_REST_Server::READABLE, 60 59 'callback' => [ $this, 'send_access_email' ], 61 60 'args' => [ 62 61 ], 63 62 'permission_callback' => 'is_user_logged_in', 64 63 ] ); 65 64 66 65 add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 ); 67 66 } 68 67 -
plugins/plugin-directory/api/routes/class-plugin-self-close.php
use WordPressdotorg\Plugin_Directory\Too 12 12 */ 13 13 class Plugin_Self_Close extends Base { 14 14 15 15 public function __construct() { 16 16 register_rest_route( 'plugins/v1', '/plugin/(?P<plugin_slug>[^/]+)/self-close', [ 17 17 'methods' => \WP_REST_Server::CREATABLE, 18 18 'callback' => [ $this, 'self_close' ], 19 19 'args' => [ 20 20 'plugin_slug' => [ 21 21 'validate_callback' => [ $this, 'validate_plugin_slug_callback' ], 22 22 ], 23 23 ], 24 24 'permission_callback' => function( $request ) { 25 25 $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] ); 26 26 27 return current_user_can( 'plugin_ admin_edit', $plugin ) && 'publish' === $plugin->post_status;27 return current_user_can( 'plugin_self_close', $plugin ); 28 28 }, 29 29 ] ); 30 30 31 31 add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 ); 32 32 } 33 33 34 34 /** 35 35 * Redirect back to the plugins page when this endpoint is accessed with an invalid nonce. 36 36 */ 37 37 function override_cookie_expired_message( $result, $obj, $request ) { 38 38 if ( 39 39 is_array( $result ) && isset( $result['code'] ) && 40 40 preg_match( '!^/plugins/v1/plugin/([^/]+)/self-close$!', $request->get_route(), $m ) 41 41 ) { 42 42 if ( 'rest_cookie_invalid_nonce' == $result['code'] ) { -
plugins/plugin-directory/api/routes/class-plugin-self-transfer.php
class Plugin_Self_Transfer extends Base 18 18 'methods' => \WP_REST_Server::CREATABLE, 19 19 'callback' => [ $this, 'self_transfer' ], 20 20 'args' => [ 21 21 'plugin_slug' => [ 22 22 'validate_callback' => [ $this, 'validate_plugin_slug_callback' ], 23 23 ], 24 24 'new_owner' => [ 25 25 'validate_callback' => function( $id ) { 26 26 return (bool) get_user_by( 'id', $id ); 27 27 } 28 28 ] 29 29 ], 30 30 'permission_callback' => function( $request ) { 31 31 $plugin = Plugin_Directory::get_plugin_post( $request['plugin_slug'] ); 32 32 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 ); 37 34 }, 38 35 ] ); 39 36 40 37 add_filter( 'rest_pre_echo_response', [ $this, 'override_cookie_expired_message' ], 10, 3 ); 41 38 } 42 39 43 40 /** 44 41 * Redirect back to the plugins page when this endpoint is accessed with an invalid nonce. 45 42 */ 46 43 function override_cookie_expired_message( $result, $obj, $request ) { 47 44 if ( 48 45 is_array( $result ) && isset( $result['code'] ) && 49 46 preg_match( '!^/plugins/v1/plugin/([^/]+)/self-transfer$!', $request->get_route(), $m ) 50 47 ) { 51 48 if ( 'rest_cookie_invalid_nonce' == $result['code'] ) { -
plugins/plugin-directory/class-capabilities.php
use WordPressdotorg\Plugin_Directory\Too 10 10 */ 11 11 class Capabilities { 12 12 13 13 /** 14 14 * Filters a user's capabilities depending on specific context and/or privilege. 15 15 * 16 16 * @static 17 17 * 18 18 * @param array $required_caps Returns the user's actual capabilities. 19 19 * @param string $cap Capability name. 20 20 * @param int $user_id The user ID. 21 21 * @param array $context Adds the context to the cap. Typically the object ID. 22 22 * @return array Primitive caps. 23 23 */ 24 24 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 } 46 40 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 } 52 46 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(); 59 49 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 } 64 80 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'; 75 104 } 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 } 76 113 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'; 90 117 } 91 118 92 return $required_caps;119 return array_unique( $required_caps ); 93 120 } 94 121 95 122 /** 96 123 * Sets up custom roles and makes them available. 97 124 * 98 125 * @static 99 126 */ 100 127 public static function add_roles() { 101 128 $reviewer = array( 102 129 'read' => true, 103 130 'plugin_set_category' => true, 104 131 'moderate_comments' => true, 105 132 'plugin_edit_pending' => true, 106 133 'plugin_review' => true, 107 134 'plugin_dashboard_access' => true, -
themes/pub/wporg-plugins/inc/template-tags.php
function the_unconfirmed_releases_notice 283 283 } 284 284 285 285 if ( ! $warning ) { 286 286 return; 287 287 } 288 288 289 289 printf( 290 290 '<div class="plugin-notice notice notice-info notice-alt"><p>%s</p></div>', 291 291 sprintf( 292 292 __( 'This plugin has <a href="%s">a pending release that requires confirmation</a>.', 'wporg-plugins' ), 293 293 home_url( '/developers/releases/' ) // TODO: Hardcoded URL. 294 294 ) 295 295 ); 296 296 } 297 297 298 function 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 298 314 /** 299 315 * Display the ADVANCED Zone. 300 316 */ 301 317 function the_plugin_advanced_zone() { 302 318 $post = get_post(); 303 319 304 320 // If the post is closed, this all goes away. 305 321 if ( 'publish' !== $post->post_status ) { 306 322 return; 307 323 } 308 324 309 325 echo '<hr>'; 310 326 311 327 echo '<h2>' . esc_html__( 'Advanced Options', 'wporg-plugins' ) . '</h2>'; 312 328 … … function the_plugin_danger_zone() { 395 411 // Output the self close button. 396 412 the_plugin_self_close_button(); 397 413 } 398 414 399 415 } 400 416 401 417 /** 402 418 * Displays a form for plugin committers to self-close a plugin. Permanently. 403 419 * It is disabled for plugins with 20,000+ users. 404 420 */ 405 421 function the_plugin_self_close_button() { 406 422 $post = get_post(); 407 423 $active_installs = (int) get_post_meta( $post->ID, 'active_installs', true ); 408 424 $close_link = false; 409 425 410 if ( ! current_user_can( 'plugin_ admin_edit', $post ) || 'publish' != $post->post_status) {426 if ( ! current_user_can( 'plugin_self_close', $post ) ) { 411 427 return; 412 428 } 413 429 414 430 echo '<h4>' . esc_html__( 'Close This Plugin', 'wporg-plugins' ) . '</h4>'; 415 431 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>'; 416 432 417 433 echo '<div class="plugin-notice notice notice-warning notice-alt"><p>'; 418 434 if ( $active_installs >= 20000 ) { 419 435 // Translators: %s is the plugin team email address. 420 436 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' ); 421 437 } else { 422 438 $close_link = Template::get_self_close_link( $post ); 423 439 _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' ); 424 440 } 425 441 echo '</p></div>'; … … function the_plugin_self_close_button() 427 443 if ( $close_link ) { 428 444 echo '<form method="POST" action="' . esc_url( $close_link ) . '" onsubmit="return confirm( jQuery(this).prev(\'.notice\').text() );">'; 429 445 // Translators: %s is the plugin name, as defined by the plugin itself. 430 446 echo '<p><input class="button" type="submit" value="' . esc_attr( sprintf( __( 'I understand, please close %s.', 'wporg-plugins' ), get_the_title() ) ) . '" /></p>'; 431 447 echo '</form>'; 432 448 } 433 449 } 434 450 435 451 /** 436 452 * Display a form to allow a plugin owner to transfer the ownership of a plugin to someone else. 437 453 * This does NOT remove their commit ability. 438 454 */ 439 455 function the_plugin_self_transfer_form() { 440 456 $post = get_post(); 441 457 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 ) ) { 446 459 return; 447 460 } 448 461 449 462 echo '<h4>' . esc_html__( 'Transfer This Plugin', 'wporg-plugins' ) . '</h4>'; 450 463 451 464 if ( get_current_user_id() != $post->post_author ) { 452 465 $owner = get_user_by( 'id', $post->post_author ); 453 466 /* translators: %s: Name of plugin owner */ 454 467 echo '<p>' . esc_html( sprintf( 455 468 __( 'This plugin is currently owned by %s, they can choose to transfer ownership rights of the plugin to you.', 'wporg-plugins' ), 456 469 $owner->display_name 457 470 ) ) . '</p>'; 458 471 return; 459 472 } 460 473 … … function the_plugin_self_transfer_form() 483 496 '<option value="%d">%s</option>' . "\n", 484 497 esc_attr( $user->ID ), 485 498 esc_html( $user->display_name . ' (' . $user->user_login . ')' ) 486 499 ); 487 500 } 488 501 echo '</select></p>'; 489 502 // Translators: %s is the plugin name, as defined by the plugin itself. 490 503 echo '<p><input class="button" type="submit" value="' . esc_attr( sprintf( __( 'Please transfer %s.', 'wporg-plugins' ), get_the_title() ) ) . '" /></p>'; 491 504 echo '</form>'; 492 505 493 506 } 494 507 495 508 function the_plugin_release_confirmation_form() { 496 509 $post = get_post(); 497 510 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 ) ) { 502 512 return; 503 513 } 504 514 505 515 $confirmations_required = $post->release_confirmation; 506 516 507 517 echo '<h4>' . esc_html__( 'Release Confirmation', 'wporg-plugins' ) . '</h4>'; 508 518 if ( $confirmations_required ) { 509 519 echo '<p>' . __( 'Release confirmations for this plugin are <strong>enabled</strong>.', 'wporg-plugins' ) . '</p>'; 510 520 } else { 511 521 echo '<p>' . __( 'Release confirmations for this plugin are <strong>disabled</strong>', 'wporg-plugins' ) . '</p>'; 512 522 } 513 523 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>'; 514 524 515 525 if ( ! $confirmations_required && 'trunk' === $post->stable_tag ) { 516 526 echo '<div class="plugin-notice notice notice-warning notice-alt"><p>'; -
themes/pub/wporg-plugins/template-parts/section-advanced.php
4 4 * 5 5 * @link https://codex.wordpress.org/Template_Hierarchy 6 6 * 7 7 * @package WordPressdotorg\Plugin_Directory\Theme 8 8 */ 9 9 10 10 namespace WordPressdotorg\Plugin_Directory\Theme; 11 11 12 12 use WordPressdotorg\Plugin_Directory\Template; 13 13 14 14 global $post; 15 15 ?> 16 16 17 17 <div id="admin" class="section"> 18 18 <?php the_closed_plugin_notice(); ?> 19 <?php the_no_self_management_notice(); ?> 19 20 20 21 <h2><?php esc_html_e( 'Statistics', 'wporg-plugins' ); ?></h2> 21 22 22 23 <h4><?php esc_html_e( 'Active versions', 'wporg-plugins' ); ?></h4> 23 24 <div id="plugin-version-stats" class="chart version-stats"></div> 24 25 25 26 <h4><?php esc_html_e( 'Downloads Per Day', 'wporg-plugins' ); ?></h4> 26 27 <div id="plugin-download-stats" class="chart download-stats"></div> 27 28 28 29 <h4><?php esc_html_e( 'Active Install Growth', 'wporg-plugins' ); ?></h4> 29 30 <div id="plugin-growth-stats" class="chart download-stats"></div> 30 31 31 32 <h4><?php esc_html_e( 'Downloads history', 'wporg-plugins' ); ?></h4> 32 33 <table id="plugin-download-history-stats" class="download-history-stats"> 33 34 <tbody></tbody>