diff --git a/includes/class-capabilities.php b/includes/class-capabilities.php new file mode 100644 index 0000000000..e55c980879 --- /dev/null +++ b/includes/class-capabilities.php @@ -0,0 +1,67 @@ + $base_cap_or_post_type ) { + $post_type_object = get_post_type_object( $cap_or_post_type ); + $post_type_object_base = get_post_type_object( $base_cap_or_post_type ); + foreach ( $caps as $requested_cap ) { + if ( stripos( $requested_cap, $cap_or_post_type ) !== false ) { + if ( $post_type_object && $post_type_object_base ) { + $base = array_search( $requested_cap, (array) $post_type_object->cap, true ); + $base_cap_or_post_type = $post_type_object_base->cap->$base; + } + if ( + $base_cap_or_post_type !== false + && isset( $allcaps[ $base_cap_or_post_type ] ) + && $allcaps[ $base_cap_or_post_type ] + ) { + $allcaps[ $requested_cap ] = true; + } + } + } + } + return $allcaps; + } + + /** + * Filter the capability-manager-enhanced (PublishPress Capabilties) plugin UI. + * + * @param array $plugin_caps Array of per-plugin caps. + */ + public static function cme_plugin_capabilities( $plugin_caps ) { + $plugin_caps['Newspack'] = apply_filters( 'newspack_capabilities_in_cme_plugin', [] ); + return $plugin_caps; + } +} +Capabilities::init(); diff --git a/includes/class-newspack.php b/includes/class-newspack.php index 96af7bf9ac..c79d93bca1 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -181,6 +181,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/class-amp-enhancements.php'; include_once NEWSPACK_ABSPATH . 'includes/class-newspack-image-credits.php'; include_once NEWSPACK_ABSPATH . 'includes/class-rss-add-image.php'; + include_once NEWSPACK_ABSPATH . 'includes/class-capabilities.php'; /* Integrations with other plugins. */ include_once NEWSPACK_ABSPATH . 'includes/plugins/class-jetpack.php'; diff --git a/includes/class-patches.php b/includes/class-patches.php index 4aadf50a67..5c720f30f6 100644 --- a/includes/class-patches.php +++ b/includes/class-patches.php @@ -368,6 +368,8 @@ public static function maybe_display_author_page( $query ) { /** * Restrict non-privileged users from seeing posts not owned by them. + * An author without the edit_others_* cap will not be able to edit the posts, + * but still can view the list of posts. This method prevents that. * Affects all admin post lists and the legacy (non-AJAX) media library list page. * * @param WP_Query $query Query to alter. @@ -385,7 +387,8 @@ public static function restrict_others_posts( $query ) { $is_posts_list = 'edit' === $current_screen->base; // If the user can't edit others' posts, only allow them to view their own posts. - if ( ( $is_media_library || $is_posts_list ) && ! current_user_can( 'edit_others_posts' ) ) { + $post_type_object = get_post_type_object( $current_screen->post_type ); + if ( ( $is_media_library || $is_posts_list ) && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) { $query->set( 'author', $current_user_id ); // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts add_filter( 'wp_count_posts', [ __CLASS__, 'fix_post_counts' ], 10, 2 ); } diff --git a/includes/optional-modules/class-rss.php b/includes/optional-modules/class-rss.php index 8235e69698..7034fbbb6c 100644 --- a/includes/optional-modules/class-rss.php +++ b/includes/optional-modules/class-rss.php @@ -13,8 +13,8 @@ * RSS feed enhancements. */ class RSS { - const FEED_CPT = 'partner_rss_feed'; - const FEED_QUERY_ARG = 'partner-feed'; + const FEED_CPT = 'partner_rss_feed'; + const FEED_QUERY_ARG = 'partner-feed'; const FEED_SETTINGS_META = 'partner_feed_settings'; /** @@ -43,6 +43,8 @@ public static function init() { add_filter( 'wpseo_include_rss_footer', [ __CLASS__, 'maybe_suppress_yoast' ] ); add_action( 'rss2_ns', [ __CLASS__, 'maybe_inject_yahoo_namespace' ] ); add_filter( 'the_title_rss', [ __CLASS__, 'maybe_wrap_titles_in_cdata' ] ); + + add_filter( 'newspack_capabilities_map', [ __CLASS__, 'newspack_capabilities_map' ] ); } /** @@ -141,7 +143,8 @@ public static function register_feed_cpt() { 'show_in_menu' => true, 'menu_icon' => 'dashicons-rss', 'query_var' => true, - 'capability_type' => 'post', + 'capability_type' => self::FEED_CPT, + 'map_meta_cap' => true, 'has_archive' => false, 'hierarchical' => false, 'menu_position' => null, @@ -506,7 +509,8 @@ public static function save_settings( $feed_post_id ) { return; } - if ( ! current_user_can( 'edit_posts' ) ) { + $post_type_object = get_post_type_object( self::FEED_CPT ); + if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) { return; } @@ -871,5 +875,16 @@ public static function maybe_wrap_titles_in_cdata( $title ) { private static function is_republication_tracker_plugin_active() { return class_exists( 'Republication_Tracker_Tool' ); } + + /** + * Map the capabilities for the RSS feed custom post type. + * + * @param array $capabilities_map The existing capabilities map. + * @return array The modified capabilities map with RSS feed CPT capabilities. + */ + public static function newspack_capabilities_map( $capabilities_map ) { + $capabilities_map[ self::FEED_CPT ] = 'post'; + return $capabilities_map; + } } RSS::init(); diff --git a/includes/wizards/audience/class-audience-campaigns.php b/includes/wizards/audience/class-audience-campaigns.php index 326b9af45b..18478b2cf9 100644 --- a/includes/wizards/audience/class-audience-campaigns.php +++ b/includes/wizards/audience/class-audience-campaigns.php @@ -28,6 +28,13 @@ class Audience_Campaigns extends Wizard { */ protected $parent_slug = 'newspack-audience'; + /** + * Required capability. + * + * @var string + */ + protected $capability = 'newspack_campaigns'; + /** * Constructor. */ @@ -39,6 +46,8 @@ public function __construct() { // Determine active menu items. add_filter( 'parent_file', [ $this, 'parent_file' ] ); add_filter( 'submenu_file', [ $this, 'submenu_file' ] ); + add_filter( 'newspack_capabilities_map', [ $this, 'newspack_capabilities_map' ] ); + add_filter( 'newspack_capabilities_in_cme_plugin', [ $this, 'newspack_capabilities_in_cme_plugin' ] ); } /** @@ -1017,4 +1026,24 @@ public function submenu_file( $submenu_file ) { return $submenu_file; } + + /** + * Map this wizard capability from 'manage_options' capability. + * + * @param array $capabilities_map Mapping of capabilities. + */ + public function newspack_capabilities_map( $capabilities_map ) { + $capabilities_map[ $this->capability ] = 'manage_options'; + return $capabilities_map; + } + + /** + * Register this capability in Newspack capabilities list in the capability-manager-enhanced plugin. + * + * @param array $capabilities Mapping of capabilities. + */ + public function newspack_capabilities_in_cme_plugin( $capabilities ) { + $capabilities[] = $this->capability; + return $capabilities; + } } diff --git a/tests/unit-tests/capabilities.php b/tests/unit-tests/capabilities.php new file mode 100644 index 0000000000..e8dad114d1 --- /dev/null +++ b/tests/unit-tests/capabilities.php @@ -0,0 +1,131 @@ + [ + // 'newspack_post' caps should inherit from 'post'. + 'newspack_post' => 'post', + ] + ); + + // Mock the post types. + $this->mock_post_type_object( 'newspack_post' ); + + $user_all_caps = [ + 'edit_posts' => true, + 'delete_posts' => false, + ]; + $required_caps = [ 'edit_newspack_posts' ]; + + $result = Capabilities::map_capabilities( $user_all_caps, $required_caps ); + $this->assertEquals( array_merge( $user_all_caps, [ 'edit_newspack_posts' => true ] ), $result, 'User with edit_posts cap should get the edit_newspack_post cap, too' ); + + $user_all_caps = [ + 'edit_posts' => false, + 'delete_posts' => false, + ]; + $result = Capabilities::map_capabilities( $user_all_caps, $required_caps ); + $this->assertEquals( $user_all_caps, $result, 'User without edit_posts cap should not get the edit_newspack_post cap' ); + + $user_all_caps = [ + 'edit_posts' => true, + 'delete_posts' => true, + ]; + $required_caps = [ 'edit_newspack_posts', 'delete_newspack_posts' ]; + $this->assertEquals( + array_merge( + $user_all_caps, + [ + 'edit_newspack_posts' => true, + 'delete_newspack_posts' => true, + ] + ), + Capabilities::map_capabilities( $user_all_caps, $required_caps ), + 'Multiple required caps are supported.' + ); + + add_filter( + 'newspack_capabilities_map', + fn() => [ + // 'newspack_post' caps should inherit from 'page'. + 'newspack_post' => 'page', + ] + ); + $result = Capabilities::map_capabilities( $user_all_caps, [ 'edit_newspack_posts' ] ); + $this->assertEquals( + false, + isset( $result['edit_newspack_posts'] ), + "User can't edit posts which inherrit caps from pages (even though they can edit posts)." + ); + } + + /** + * Test the map_capabilities method. + */ + public function test_capabilities_mapping_other_caps() { + add_filter( + 'newspack_capabilities_map', + fn() => [ + // 'newspack_widgets' caps should inherit from 'manage_options'. + 'newspack_widgets' => 'manage_options', + ] + ); + $user_all_caps = [ + 'manage_options' => true, + ]; + $this->assertEquals( + array_merge( $user_all_caps, [ 'newspack_widgets' => true ] ), + Capabilities::map_capabilities( $user_all_caps, [ 'newspack_widgets' ] ), + 'User who can manage_options can newspack_widgets too.' + ); + $user_all_caps = [ + 'manage_options' => false, + ]; + $this->assertEquals( + $user_all_caps, + Capabilities::map_capabilities( $user_all_caps, [ 'newspack_widgets' ] ), + "User who can't manage_options can't newspack_widgets neither." + ); + } + + /** + * Mock a post type object. + * + * @param string $post_type Post type name. + */ + private function mock_post_type_object( $post_type ) { + $capabilities = [ + 'edit_posts' => 'edit_' . $post_type . 's', + 'delete_posts' => 'delete_' . $post_type . 's', + ]; + add_filter( + 'register_post_type_args', + function( $args, $name ) use ( $post_type, $capabilities ) { + if ( $name === $post_type ) { + $args['cap'] = $capabilities; + } + return $args; + }, + 10, + 2 + ); + + register_post_type( $post_type ); + } +}