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