Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Dashboard: Hide the Subscribers tab on sites where the Jetpack subscriptions module is turned off.
30 changes: 22 additions & 8 deletions projects/packages/premium-analytics/src/dashboard-layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,38 @@ function get_dashboard_default_layout_for( $dashboard_name ) {
);
}

/**
* Returns availability gates for conditional dashboard tabs.
*
* The callbacks are used directly because the section registry may not be
* initialized when this route runs.
*
* @since $$next-version$$
*
* @return callable[] Resolved tab ID mapped to its availability callback.
*/
function get_dashboard_default_layout_gates() {
return array(
DASHBOARD_STORE_SECTION_ID => array( Capabilities::class, 'current_user_can_view_store_reports' ),
DASHBOARD_SUBSCRIBERS_SECTION_ID => __NAMESPACE__ . '\\is_subscribers_dashboard_section_available',
);
}

/**
* REST callback returning the default layout for the requested dashboard.
*
* The route admits every dashboard reader, but its name also resolves to a
* single tab, so the store tab is refused here the way the section route
* refuses it through {@see Dashboard_Section::is_available()}. The check keys
* on the resolved tab rather than the string because two spellings arrive: the
* bare `store` alias in the URL, and `?name=woocommerce/store`, which WordPress
* reads ahead of the URL capture.
* Availability is checked after resolving the name because tabs can be
* requested by alias or full section ID.
*
* @param \WP_REST_Request $request REST request carrying the dashboard name.
* @return \WP_REST_Response|\WP_Error Response wrapping the default layout array.
*/
function get_dashboard_default_layout_response( $request ) {
$dashboard_name = $request['name'];
$gates = get_dashboard_default_layout_gates();
$section_id = get_dashboard_default_section_id_for( $dashboard_name );

if ( DASHBOARD_STORE_SECTION_ID === get_dashboard_default_section_id_for( $dashboard_name )
&& ! Capabilities::current_user_can_view_store_reports() ) {
if ( isset( $gates[ $section_id ] ) && ! call_user_func( $gates[ $section_id ] ) ) {
return new \WP_Error(
'dashboard_section_unavailable',
__( 'Dashboard section is not available.', 'jetpack-premium-analytics-pkg' ),
Expand Down
31 changes: 31 additions & 0 deletions projects/packages/premium-analytics/src/dashboard-sections.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

namespace Automattic\Jetpack\PremiumAnalytics;

use Automattic\Jetpack\Modules;

require_once __DIR__ . '/dashboard-layout.php';
require_once __DIR__ . '/dashboard-grammar.php';
require_once __DIR__ . '/class-dashboard-section.php';
Expand All @@ -17,6 +19,11 @@
*/
const WOOCOMMERCE_DASHBOARD_SECTION_AVAILABLE_FILTER = 'jetpack_premium_analytics_woocommerce_dashboard_section_available';

/**
* Filter through which Subscribers section availability is resolved.
*/
const SUBSCRIBERS_DASHBOARD_SECTION_AVAILABLE_FILTER = 'jetpack_premium_analytics_subscribers_dashboard_section_available';

/**
* Registers a dashboard section.
*
Expand Down Expand Up @@ -80,6 +87,29 @@ function is_woocommerce_dashboard_section_available_to_current_user() {
return is_woocommerce_dashboard_section_available() && Capabilities::current_user_can_view_store_reports();
}

/**
* Whether the Subscribers dashboard section should be exposed.
*
* Sites without Jetpack have no module state to check, so the section remains
* available. Modules::is_active() also returns true on WPCOM Simple.
*
* @since $$next-version$$
*
* @return bool True when the subscriptions module is active.
*/
function is_subscribers_dashboard_section_available() {
$is_available = ! class_exists( 'Jetpack' ) || ( new Modules() )->is_active( 'subscriptions' );

/**
* Filters whether the Subscribers dashboard section is available.
*
* @since $$next-version$$
*
* @param bool $is_available Whether the subscriptions module was detected in the current request.
*/
return (bool) apply_filters( SUBSCRIBERS_DASHBOARD_SECTION_AVAILABLE_FILTER, $is_available );
}

/**
* Returns the default widget layout for the WooCommerce dashboard section.
*
Expand Down Expand Up @@ -127,6 +157,7 @@ function register_default_dashboard_sections() {
'title' => __( 'Subscribers stats', 'jetpack-premium-analytics-pkg' ),
'description' => __( 'How your subscriber list is growing, and how your emails land.', 'jetpack-premium-analytics-pkg' ),
'order' => 30,
'is_available' => __NAMESPACE__ . '\\is_subscribers_dashboard_section_available',
'default_layout' => static function () {
return get_dashboard_default_layout_for( 'analytics/subscribers' );
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ function filter_registrable_widget_types_by_plugin( $widget_candidates ) {

add_filter( REGISTRABLE_WIDGET_TYPES_FILTER, __NAMESPACE__ . '\\filter_registrable_widget_types_by_plugin' );

// Subscriber widgets remain available when their section is hidden because
// their data does not depend on the local module. Unregistering them would also
// break instances placed in other sections.

/**
* Removes candidates the reader could not load data for anyway.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use WP_REST_Server;

require_once __DIR__ . '/../../src/dashboard-layout.php';
require_once __DIR__ . '/../../src/dashboard-sections.php';
require_once __DIR__ . '/traits/trait-analytics-capabilities.php';

/**
Expand All @@ -33,6 +34,7 @@ public function tear_down() {
$wp_rest_server = null;
$this->reset_analytics_capabilities();
wp_set_current_user( 0 );
remove_all_filters( SUBSCRIBERS_DASHBOARD_SECTION_AVAILABLE_FILTER );
Constants::clear_constants();
parent::tear_down();
}
Expand Down Expand Up @@ -160,6 +162,45 @@ public function test_default_layout_route_serves_the_traffic_tab_to_a_view_stats
$this->assertNotContains( 'jpa/store-performance', $types );
}

/**
* An unavailable tab does not expose its default layout.
*/
public function test_default_layout_route_refuses_an_unavailable_subscribers_tab() {
$this->register_route_with_capabilities();
$this->grant_view_stats_to( $this->login_as( 'editor' ) );
add_filter( SUBSCRIBERS_DASHBOARD_SECTION_AVAILABLE_FILTER, '__return_false' );

list( $status ) = $this->request_default_layout( DASHBOARD_SUBSCRIBERS_SECTION_ID );

$this->assertSame( 404, $status );
}

/**
* An unavailable tab is refused when `?name=` shadows the URL capture.
*/
public function test_default_layout_route_refuses_a_name_shadowed_subscribers_tab() {
$this->register_route_with_capabilities();
$this->grant_view_stats_to( $this->login_as( 'editor' ) );
add_filter( SUBSCRIBERS_DASHBOARD_SECTION_AVAILABLE_FILTER, '__return_false' );

list( $status ) = $this->request_default_layout( DASHBOARD_NAME, 'analytics/subscribers' );

$this->assertSame( 404, $status );
}

/**
* An available Subscribers tab exposes its default layout.
*/
public function test_default_layout_route_serves_an_available_subscribers_tab() {
$this->register_route_with_capabilities();
$this->grant_view_stats_to( $this->login_as( 'editor' ) );

list( $status, $types ) = $this->request_default_layout( DASHBOARD_SUBSCRIBERS_SECTION_ID );

$this->assertSame( 200, $status );
$this->assertContains( 'jpa/subscribers-chart', $types );
}

/**
* Non-Premium-Analytics dashboards are left untouched.
*/
Expand Down
Loading
Loading