Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.71% covered (success)
95.71%
156 / 163
100.00% covered (success)
100.00%
14 / 14
CRAP
n/a
0 / 0
Automattic\Jetpack\PremiumAnalytics\register_dashboard_section
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_registered_dashboard_section
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_available_dashboard_sections
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_available_dashboard_section_slugs
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\configure_dashboard_sections_script_data
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\is_dashboard_section_in_preview_scope
n/a
0 / 0
n/a
0 / 0
1
Automattic\Jetpack\PremiumAnalytics\configure_dashboard_preview_scope
n/a
0 / 0
n/a
0 / 0
1
Automattic\Jetpack\PremiumAnalytics\inject_dashboard_sections_script_data
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
Automattic\Jetpack\PremiumAnalytics\check_dashboard_sections_permission
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_available_dashboard_section_for_route
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
Automattic\Jetpack\PremiumAnalytics\get_dashboard_section_schema
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_dashboard_sections_response
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\PremiumAnalytics\get_dashboard_section_default_layout_response
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\PremiumAnalytics\register_dashboard_sections_rest_routes
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Dashboard Sections API: the registry helpers, the section script data, and the REST routes.
4 *
5 * The package's own sections register through this API from default-dashboard-sections.php,
6 * the same way a plugin extending the dashboard does.
7 *
8 * @package automattic/jetpack-premium-analytics
9 */
10
11namespace Automattic\Jetpack\PremiumAnalytics;
12
13require_once __DIR__ . '/dashboard-layout.php';
14require_once __DIR__ . '/dashboard-grammar.php';
15require_once __DIR__ . '/rest-namespace.php';
16require_once __DIR__ . '/class-dashboard-section.php';
17require_once __DIR__ . '/class-dashboard-section-registry.php';
18
19// Guarded on a symbol the file declares, so a second copy of the package can't redeclare it.
20if ( ! function_exists( __NAMESPACE__ . '\\register_dashboard_feature_flags' ) ) {
21    require_once __DIR__ . '/dashboard-policy.php';
22}
23
24/**
25 * Registers a dashboard section.
26 *
27 * @param string $dashboard_name Dashboard identifier.
28 * @param string $id             Section identifier.
29 * @param array  $args           Optional. Section arguments.
30 * @return Dashboard_Section|false The registered section on success, or false on failure.
31 */
32function register_dashboard_section( $dashboard_name, $id, $args = array() ) {
33    return Dashboard_Section_Registry::get_instance()->register( $dashboard_name, $id, $args );
34}
35
36/**
37 * Retrieves a registered dashboard section.
38 *
39 * @param string $dashboard_name Dashboard identifier.
40 * @param string $id             Section identifier.
41 * @return Dashboard_Section|null The registered section, or null when absent.
42 */
43function get_registered_dashboard_section( $dashboard_name, $id ) {
44    return Dashboard_Section_Registry::get_instance()->get_registered( $dashboard_name, $id );
45}
46
47/**
48 * Retrieves available dashboard sections.
49 *
50 * @param string $dashboard_name Dashboard identifier.
51 * @return Dashboard_Section[] Ordered list of available sections.
52 */
53function get_available_dashboard_sections( $dashboard_name ) {
54    return Dashboard_Section_Registry::get_instance()->get_available_sections( $dashboard_name );
55}
56
57/**
58 * Slugs of the tabs the dashboard exposes, for the client's report routes.
59 *
60 * Reads the same sections the tab list does, so a report cannot outlive the tab it sits
61 * behind. Null, never `array()`, while nothing is registered: an empty array means no
62 * tab is available.
63 *
64 * @since 0.6.0
65 *
66 * @return string[]|null
67 */
68function get_available_dashboard_section_slugs() {
69    $registry = Dashboard_Section_Registry::get_instance();
70
71    if ( empty( $registry->get_all_registered( DASHBOARD_NAME ) ) ) {
72        return null;
73    }
74
75    return array_map(
76        static function ( Dashboard_Section $section ) {
77            return $section->slug;
78        },
79        $registry->get_available_sections( DASHBOARD_NAME )
80    );
81}
82
83/**
84 * Configures the section script data.
85 *
86 * @since 0.6.0
87 *
88 * @return void
89 */
90function configure_dashboard_sections_script_data() {
91    add_filter( 'jetpack_admin_js_script_data', __NAMESPACE__ . '\\inject_dashboard_sections_script_data', 20 );
92}
93
94/**
95 * Kept for older copies of the package, whose Dashboard_Section::is_available() calls it on every
96 * availability check. Every section is in scope now, so the section's own rule decides.
97 *
98 * @since 0.6.0
99 * @deprecated 0.10.0 The preview scope is gone.
100 *
101 * @param string $dashboard_name Dashboard identifier.
102 * @param string $slug           URL-facing section slug.
103 * @return bool Always true.
104 */
105function is_dashboard_section_in_preview_scope( $dashboard_name, $slug ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Kept for the signature older copies call.
106    return true;
107}
108
109/**
110 * Kept for older copies of the package: they guard their include of this file on another
111 * symbol and call this, so a newer copy loading first must still define it.
112 *
113 * @since 0.6.0
114 * @deprecated 0.10.0 Use configure_dashboard_sections_script_data().
115 *
116 * @return void
117 */
118function configure_dashboard_preview_scope() {
119    configure_dashboard_sections_script_data();
120}
121
122/**
123 * Injects the available section slugs, and whether the reader may see Stats, into JetpackScriptData.
124 *
125 * The same list travels over REST for the tab bar, but a report route reads no REST before
126 * choosing its redirect, so it reads the slugs from boot data instead.
127 *
128 * @since 0.6.0
129 *
130 * @param array $data The script data passed by the assets package.
131 * @return array
132 */
133function inject_dashboard_sections_script_data( array $data ): array {
134    if ( ! isset( $data['premium_analytics'] ) || ! is_array( $data['premium_analytics'] ) ) {
135        $data['premium_analytics'] = array();
136    }
137
138    // Surfaces outside any section, such as feedback, post to Stats endpoints.
139    $data['premium_analytics']['can_view_stats'] = Capabilities::current_user_can_view_stats();
140
141    $sections = get_available_dashboard_section_slugs();
142
143    if ( null !== $sections ) {
144        $data['premium_analytics']['sections'] = $sections;
145    }
146
147    return $data;
148}
149
150/**
151 * Whether the current user can access dashboard section routes.
152 *
153 * @return bool
154 */
155function check_dashboard_sections_permission() {
156    return Capabilities::current_user_can_view_analytics();
157}
158
159/**
160 * Resolves a route section, including availability checks.
161 *
162 * @param string $dashboard_name Dashboard identifier.
163 * @param string $section_id     Section identifier.
164 * @return Dashboard_Section|\WP_Error Registered available section, or error.
165 */
166function get_available_dashboard_section_for_route( $dashboard_name, $section_id ) {
167    $section = get_registered_dashboard_section( $dashboard_name, $section_id );
168
169    if ( ! $section ) {
170        return new \WP_Error(
171            'dashboard_section_not_found',
172            __( 'Dashboard section not found.', 'jetpack-premium-analytics-pkg' ),
173            array( 'status' => 404 )
174        );
175    }
176
177    if ( ! $section->is_available() ) {
178        return new \WP_Error(
179            'dashboard_section_unavailable',
180            __( 'Dashboard section is not available.', 'jetpack-premium-analytics-pkg' ),
181            array( 'status' => 404 )
182        );
183    }
184
185    return $section;
186}
187
188/**
189 * REST schema for one dashboard section, as returned by the sections route.
190 *
191 * Mirrored by the frontend's `sections.ts` and reused by WPCOM for Simple sites (see AGENTS.md).
192 *
193 * @since 0.2.0
194 *
195 * @return array The JSON schema for a dashboard section.
196 */
197function get_dashboard_section_schema() {
198    return array(
199        '$schema'    => 'http://json-schema.org/draft-04/schema#',
200        'title'      => 'jetpack-premium-analytics-dashboard-section',
201        'type'       => 'object',
202        'properties' => array(
203            'id'                  => array(
204                'description' => __( 'Namespaced section identifier.', 'jetpack-premium-analytics-pkg' ),
205                'type'        => 'string',
206                'readonly'    => true,
207            ),
208            'slug'                => array(
209                'description' => __( 'URL-facing section slug, derived from the identifier.', 'jetpack-premium-analytics-pkg' ),
210                'type'        => 'string',
211                'readonly'    => true,
212            ),
213            'label'               => array(
214                'description' => __( 'Translated display label, naming the section tab.', 'jetpack-premium-analytics-pkg' ),
215                'type'        => 'string',
216                'readonly'    => true,
217            ),
218            'title'               => array(
219                'description' => __( 'Translated section heading, distinct from the tab label. Null falls back to the label.', 'jetpack-premium-analytics-pkg' ),
220                'type'        => array( 'string', 'null' ),
221                'readonly'    => true,
222            ),
223            'order'               => array(
224                'description' => __( 'Sort order, ascending.', 'jetpack-premium-analytics-pkg' ),
225                'type'        => 'integer',
226                'readonly'    => true,
227            ),
228            'date_filter'         => array(
229                'description' => __( 'Which shape the section date filter takes: the rolling date range, or all time plus single years.', 'jetpack-premium-analytics-pkg' ),
230                'type'        => 'string',
231                'enum'        => Dashboard_Section::DATE_FILTERS,
232                'default'     => Dashboard_Section::DATE_FILTER_RANGE,
233                'readonly'    => true,
234            ),
235            'date_filter_options' => array(
236                'description' => __( 'What the section date filter supports, and where it renders.', 'jetpack-premium-analytics-pkg' ),
237                'type'        => 'object',
238                'properties'  => array(
239                    'with_date_comparison'         => array(
240                        'description' => __( 'Whether the section supports period-over-period comparison at all. When false, no widget in the section receives comparison parameters.', 'jetpack-premium-analytics-pkg' ),
241                        'type'        => 'boolean',
242                        'default'     => true,
243                    ),
244                    'with_header_date_control'     => array(
245                        'description' => __( 'Whether the section header renders the date control. When false, the section widgets host their own.', 'jetpack-premium-analytics-pkg' ),
246                        'type'        => 'boolean',
247                        'default'     => true,
248                    ),
249                    'with_header_interval_control' => array(
250                        'description' => __( 'Whether the section header renders the chart interval control. When false, the section charts host their own.', 'jetpack-premium-analytics-pkg' ),
251                        'type'        => 'boolean',
252                        'default'     => true,
253                    ),
254                ),
255                'readonly'    => true,
256            ),
257            'requires_sync'       => array(
258                'description' => __( 'Whether the section\'s numbers stay incomplete until the analytics initial full sync has finished.', 'jetpack-premium-analytics-pkg' ),
259                'type'        => 'boolean',
260                'default'     => false,
261                'readonly'    => true,
262            ),
263            'default_layout'      => array(
264                'description' => __( 'Bundled default widget layout.', 'jetpack-premium-analytics-pkg' ),
265                'type'        => 'array',
266                'items'       => array( 'type' => 'object' ),
267                'readonly'    => true,
268            ),
269        ),
270    );
271}
272
273/**
274 * REST callback returning available dashboard sections.
275 *
276 * @param \WP_REST_Request $request REST request carrying the dashboard name.
277 * @return \WP_REST_Response
278 */
279function get_dashboard_sections_response( $request ) {
280    $sections = array_map(
281        static function ( Dashboard_Section $section ) {
282            return $section->to_array();
283        },
284        get_available_dashboard_sections( $request['name'] )
285    );
286
287    return rest_ensure_response( $sections );
288}
289
290/**
291 * REST callback returning a section's default layout.
292 *
293 * @param \WP_REST_Request $request REST request carrying dashboard and section identifiers.
294 * @return \WP_REST_Response|\WP_Error
295 */
296function get_dashboard_section_default_layout_response( $request ) {
297    $section = get_available_dashboard_section_for_route( $request['name'], $request['section'] );
298
299    if ( is_wp_error( $section ) ) {
300        return $section;
301    }
302
303    return rest_ensure_response( $section->get_default_layout() );
304}
305
306/**
307 * Registers dashboard section REST routes.
308 *
309 * @return void
310 */
311function register_dashboard_sections_rest_routes() {
312    register_rest_route(
313        DASHBOARD_REST_NAMESPACE,
314        '/dashboards/(?P<name>' . get_dashboard_name_pattern() . ')/sections',
315        array(
316            array(
317                // @phan-suppress-next-line PhanPluginMixedKeyNoKey -- register_rest_route()'s own signature mixes a numerically keyed endpoint list with a route-level `schema` key.
318                'methods'             => \WP_REST_Server::READABLE,
319                'callback'            => __NAMESPACE__ . '\\get_dashboard_sections_response',
320                'permission_callback' => __NAMESPACE__ . '\\check_dashboard_sections_permission',
321                'args'                => array(
322                    'name' => array(
323                        'description' => __( 'Dashboard identifier as produced by the build pipeline.', 'jetpack-premium-analytics-pkg' ),
324                        'type'        => 'string',
325                    ),
326                ),
327            ),
328            'schema' => __NAMESPACE__ . '\\get_dashboard_section_schema',
329        )
330    );
331
332    register_rest_route(
333        DASHBOARD_REST_NAMESPACE,
334        '/dashboards/(?P<name>' . get_dashboard_name_pattern() . ')/sections/(?P<section>' . get_dashboard_section_id_pattern() . ')/default-layout',
335        array(
336            'methods'             => \WP_REST_Server::READABLE,
337            'callback'            => __NAMESPACE__ . '\\get_dashboard_section_default_layout_response',
338            'permission_callback' => __NAMESPACE__ . '\\check_dashboard_sections_permission',
339            'args'                => array(
340                'name'    => array(
341                    'description' => __( 'Dashboard identifier as produced by the build pipeline.', 'jetpack-premium-analytics-pkg' ),
342                    'type'        => 'string',
343                ),
344                'section' => array(
345                    'description' => __( 'Dashboard section identifier.', 'jetpack-premium-analytics-pkg' ),
346                    'type'        => 'string',
347                ),
348            ),
349        )
350    );
351}