Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions includes/class-capabilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* Newspack Capabilities.
*
* @package Newspack
*/

namespace Newspack;

defined( 'ABSPATH' ) || exit;

/**
* Newspack Capabilities Class.
*/
final class Capabilities {
/**
* Initialize Hooks.
*/
public static function init() {
add_filter( 'user_has_cap', [ __CLASS__, 'map_capabilities' ], 10, 2 );
add_filter( 'cme_plugin_capabilities', [ __CLASS__, 'cme_plugin_capabilities' ] );
}

/**
* Map legacy capabilities to granularly-controlled capabilities.
* This allows custom post type capabilities to be mapped from regular post
* capabilities. This way, when a custom post type becomes more granularily controlled
* with its own caps, users won't need to have their capabilities updated.
*
* @param bool[] $allcaps All caps.
* @param string[] $caps Required primitive capabilities for the requested capability.
*/
public static function map_capabilities( $allcaps, $caps ) {
$capabilities_map = apply_filters( 'newspack_capabilities_map', [] );
foreach ( $capabilities_map as $cap_or_post_type => $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();
1 change: 1 addition & 0 deletions includes/class-newspack.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion includes/class-patches.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 );
}
Expand Down
23 changes: 19 additions & 4 deletions includes/optional-modules/class-rss.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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' ] );
}

/**
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
29 changes: 29 additions & 0 deletions includes/wizards/audience/class-audience-campaigns.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class Audience_Campaigns extends Wizard {
*/
protected $parent_slug = 'newspack-audience';

/**
* Required capability.
*
* @var string
*/
protected $capability = 'newspack_campaigns';

/**
* Constructor.
*/
Expand All @@ -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' ] );
}

/**
Expand Down Expand Up @@ -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;
}
}
131 changes: 131 additions & 0 deletions tests/unit-tests/capabilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
/**
* Test_Capabilities class.
*
* @package Newspack
*/

use Newspack\Capabilities;

/**
* Class Test_Capabilities
*/
class Test_Capabilities extends WP_UnitTestCase {
/**
* Test the map_capabilities method.
*/
public function test_capabilities_mapping_post_to_post() {
// Use the capabilities map filter.
add_filter(
'newspack_capabilities_map',
fn() => [
// '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 );
}
}