Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.16% covered (success)
90.16%
55 / 61
100.00% covered (success)
100.00%
8 / 8
CRAP
n/a
0 / 0
Automattic\Jetpack\PremiumAnalytics\remove_dev_only_widget_types
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\filter_registrable_widget_types_by_environment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\filter_registrable_widget_types_by_availability
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\remove_plugin_gated_widget_types
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
Automattic\Jetpack\PremiumAnalytics\is_bookings_plugin_active
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
Automattic\Jetpack\PremiumAnalytics\filter_registrable_widget_types_by_plugin
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\remove_capability_gated_widget_types
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
Automattic\Jetpack\PremiumAnalytics\filter_registrable_widget_types_by_capability
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Widget availability policy (consumer layer): drops developer-only, platform-unsupported,
4 * plugin-gated and capability-gated candidates from a manifest at registry time, over the
5 * neutral hooks in widget-types.php. A dropped candidate never registers, so every reader sees
6 * the same set; a type registered one by one with register_widget_type() skips this policy.
7 *
8 * @package automattic/jetpack-premium-analytics
9 */
10
11namespace Automattic\Jetpack\PremiumAnalytics;
12
13require_once __DIR__ . '/widget-types.php';
14require_once __DIR__ . '/widget-type-support.php';
15
16/**
17 * Widget categories that are only meaningful with WooCommerce active.
18 */
19const WOOCOMMERCE_WIDGET_CATEGORIES = array( 'store', 'orders', 'coupons' );
20
21/**
22 * Widget categories that are only meaningful with WooCommerce Bookings active.
23 *
24 * Checked independently of WOOCOMMERCE_WIDGET_CATEGORIES: the Bookings
25 * extension cannot run without WooCommerce, so its presence implies both.
26 */
27const WOOCOMMERCE_BOOKINGS_WIDGET_CATEGORIES = array( 'bookings' );
28
29/**
30 * Widget categories whose data counts as a store report — by data source, not subject
31 * matter: each reaches WPCOM via the proxy's `analytics` prefix (gated on
32 * `view_woocommerce_reports`), including `visitors`, which reads `sessions/…` from it.
33 */
34const STORE_REPORT_WIDGET_CATEGORIES = array( 'store', 'orders', 'coupons', 'bookings', 'visitors' );
35
36/**
37 * Widget categories whose data counts as a Stats report: each reaches WPCOM through a proxy
38 * prefix gated on `view_stats`.
39 */
40const STATS_REPORT_WIDGET_CATEGORIES = array( 'stats', 'traffic', 'subscribers' );
41
42/**
43 * Removes developer-only candidates in production.
44 *
45 * Split from the hook callback so both branches are testable without touching
46 * the global environment.
47 *
48 * @param array  $widget_candidates Manifest candidates, each with a `name` and `category`.
49 * @param string $environment       Site environment type.
50 * @return array The candidates, minus developer-only types in production.
51 */
52function remove_dev_only_widget_types( $widget_candidates, $environment ) {
53    if ( 'production' !== $environment ) {
54        return $widget_candidates;
55    }
56
57    return array_values(
58        array_filter(
59            $widget_candidates,
60            static function ( $widget ) {
61                return 'developer' !== ( $widget['category'] ?? '' );
62            }
63        )
64    );
65}
66
67/**
68 * Registry-time callback: hides developer-only types in production.
69 *
70 * Defaults to `production`; a site opts in via `WP_ENVIRONMENT_TYPE`
71 * (`local`, `development`, `staging`).
72 *
73 * @param array $widget_candidates Manifest candidates.
74 * @return array The candidates, minus developer-only types in production.
75 */
76function filter_registrable_widget_types_by_environment( $widget_candidates ) {
77    return remove_dev_only_widget_types( $widget_candidates, wp_get_environment_type() );
78}
79
80add_filter( REGISTRABLE_WIDGET_TYPES_FILTER, __NAMESPACE__ . '\\filter_registrable_widget_types_by_environment' );
81
82/**
83 * Applies shared type-level availability at registry time.
84 *
85 * @param array $widget_candidates Manifest candidates.
86 * @return array Filtered candidates.
87 */
88function filter_registrable_widget_types_by_availability( $widget_candidates ) {
89    return remove_unsupported_widget_items(
90        $widget_candidates,
91        'name',
92        get_widget_support_context()
93    );
94}
95
96add_filter( REGISTRABLE_WIDGET_TYPES_FILTER, __NAMESPACE__ . '\\filter_registrable_widget_types_by_availability' );
97
98/**
99 * Removes candidates whose commerce category lacks its backing plugin.
100 *
101 * Split from the hook callback so the branches are testable without touching
102 * global plugin state.
103 *
104 * @param array $widget_candidates     Manifest candidates, each with a `category`.
105 * @param bool  $woocommerce_available Whether WooCommerce is available.
106 * @param bool  $bookings_available    Whether WooCommerce Bookings is available.
107 * @return array The candidates, minus commerce categories missing their plugin.
108 */
109function remove_plugin_gated_widget_types( $widget_candidates, $woocommerce_available, $bookings_available ) {
110    return array_values(
111        array_filter(
112            $widget_candidates,
113            static function ( $widget ) use ( $woocommerce_available, $bookings_available ) {
114                $category = $widget['category'] ?? '';
115
116                if ( ! $woocommerce_available && in_array( $category, WOOCOMMERCE_WIDGET_CATEGORIES, true ) ) {
117                    return false;
118                }
119
120                if ( ! $bookings_available && in_array( $category, WOOCOMMERCE_BOOKINGS_WIDGET_CATEGORIES, true ) ) {
121                    return false;
122                }
123
124                return true;
125            }
126        )
127    );
128}
129
130/**
131 * Whether the WooCommerce Bookings extension is active.
132 *
133 * Mirrors the detection in woocommerce-analytics' Bookings sync module;
134 * `is_plugin_active()` only exists in admin contexts, hence the guard.
135 *
136 * @return bool Whether WooCommerce Bookings was detected in the current request.
137 */
138function is_bookings_plugin_active() {
139    return class_exists( 'WC_Bookings' )
140        || ( function_exists( 'is_plugin_active' ) && \is_plugin_active( 'woocommerce-bookings/woocommerce-bookings.php' ) );
141}
142
143/**
144 * Registry-time callback: hides commerce categories without their plugin, reading
145 * WooCommerce availability through the store section's signal so section and widgets
146 * agree; both entry points load default-dashboard-sections.php before the registry hydrates.
147 *
148 * @param array $widget_candidates Manifest candidates.
149 * @return array The candidates, minus commerce categories missing their plugin.
150 */
151function filter_registrable_widget_types_by_plugin( $widget_candidates ) {
152    return remove_plugin_gated_widget_types(
153        $widget_candidates,
154        is_woocommerce_dashboard_section_available(),
155        is_bookings_plugin_active()
156    );
157}
158
159add_filter( REGISTRABLE_WIDGET_TYPES_FILTER, __NAMESPACE__ . '\\filter_registrable_widget_types_by_plugin' );
160
161// Subscriber widgets stay registered even when their section is hidden: their data
162// doesn't depend on the local module, and unregistering would break instances placed elsewhere.
163
164/**
165 * Removes candidates the reader could not load data for anyway.
166 *
167 * Split from the hook callback so both branches are testable without a user.
168 *
169 * @since 0.1.0
170 * @since $$next-version$$ Added `$can_view_stats`.
171 *
172 * @param array $widget_candidates      Manifest candidates, each with a `category`.
173 * @param bool  $can_view_store_reports Whether the reader may see the store reports.
174 * @param bool  $can_view_stats         Whether the reader may see the Stats reports.
175 * @return array The candidates, minus the categories whose reports the reader can't see.
176 */
177function remove_capability_gated_widget_types( $widget_candidates, $can_view_store_reports, $can_view_stats = true ) {
178    $hidden_categories = array_merge(
179        $can_view_store_reports ? array() : STORE_REPORT_WIDGET_CATEGORIES,
180        $can_view_stats ? array() : STATS_REPORT_WIDGET_CATEGORIES
181    );
182
183    if ( ! $hidden_categories ) {
184        return $widget_candidates;
185    }
186
187    return array_values(
188        array_filter(
189            $widget_candidates,
190            static function ( $widget ) use ( $hidden_categories ) {
191                return ! in_array( $widget['category'] ?? '', $hidden_categories, true );
192            }
193        )
194    );
195}
196
197/**
198 * Registry-time callback: hides widgets whose data the reader cannot fetch — a `view_stats`
199 * reader would only collect 403s from the proxy's `analytics` prefix, and a shop manager from
200 * the Stats prefixes. The registry is request-scoped, so filtering on the current user is safe here.
201 *
202 * @since 0.1.0
203 *
204 * @param array $widget_candidates Manifest candidates.
205 * @return array The candidates, minus the categories whose reports the reader can't see.
206 */
207function filter_registrable_widget_types_by_capability( $widget_candidates ) {
208    return remove_capability_gated_widget_types(
209        $widget_candidates,
210        Capabilities::current_user_can_view_store_reports(),
211        Capabilities::current_user_can_view_stats()
212    );
213}
214
215add_filter( REGISTRABLE_WIDGET_TYPES_FILTER, __NAMESPACE__ . '\\filter_registrable_widget_types_by_capability' );