Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.88% covered (warning)
77.88%
81 / 104
55.56% covered (warning)
55.56%
10 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dashboard
79.41% covered (warning)
79.41%
81 / 102
55.56% covered (warning)
55.56%
10 / 18
48.95
0.00% covered (danger)
0.00%
0 / 1
 register_feature_flag
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 init
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 register_sections
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 register_section
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 register_rest_routes
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 can_manage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_initial_state
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 add_script_data
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 maybe_load_wp_build
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 is_dashboard_request
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 load_wp_build
83.33% covered (warning)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
5.12
 alias_screen_id
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 restore_screen_id
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 enqueue_i18n_loader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 add_menu
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 add_wp_admin_submenu
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 render
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
2.04
1<?php
2/**
3 * Monetize dashboard: a wp-build page in the Jetpack sidebar.
4 *
5 * @package automattic/jetpack-monetize
6 */
7
8namespace Automattic\Jetpack\Monetize;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\Feature_Flags\Feature_Flags;
12use Automattic\Jetpack\Status\Host;
13use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
14use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
15
16if ( ! defined( 'ABSPATH' ) ) {
17    exit( 0 );
18}
19
20/**
21 * Registers the Monetize sidebar item and renders its wp-build route.
22 */
23class Dashboard {
24
25    /**
26     * Package version, bumped at release through composer.json `version-constants`.
27     *
28     * @var string
29     */
30    const PACKAGE_VERSION = '0.1.0';
31
32    /**
33     * Feature flag that makes the dashboard available while it is built.
34     *
35     * @var string
36     */
37    const FEATURE_FLAG = 'jetpack-monetize-dashboard';
38
39    /**
40     * Sidebar menu slug.
41     *
42     * @var string
43     */
44    const MENU_SLUG = 'jetpack-monetize';
45
46    /**
47     * The wp-build route's page id. It must not be the menu slug: the generated
48     * standalone page.php intercepts `admin_init` for its own id and exits.
49     *
50     * @var string
51     */
52    const WP_BUILD_PAGE_ID = 'jetpack-monetize-dashboard';
53
54    /**
55     * Name of the wp-build render function generated for WP_BUILD_PAGE_ID.
56     *
57     * @var string
58     */
59    const RENDER_FUNCTION = 'jetpack_monetize_jetpack_monetize_dashboard_wp_admin_render_page';
60
61    /**
62     * The dashboard's sections.
63     *
64     * @var string[]
65     */
66    const SECTIONS = array(
67        Sections\Payments::class,
68    );
69
70    /**
71     * Extra Admin_Menu item options, such as the module that owns the item.
72     *
73     * @var array
74     */
75    private static $menu_options = array();
76
77    /**
78     * The screen ID alias_screen_id() replaced, until it is restored.
79     *
80     * @var string|null
81     */
82    private static $original_screen_id = null;
83
84    /**
85     * Registered sections, keyed by section key.
86     *
87     * @var Dashboard_Section[]
88     */
89    private static $sections = array();
90
91    /**
92     * Register the flag. Hosts call this before they decide whether to load the dashboard.
93     *
94     * @return void
95     */
96    public static function register_feature_flag() {
97        Feature_Flags::register(
98            self::FEATURE_FLAG,
99            array(
100                'default'     => false,
101                'description' => 'Make the Monetize dashboard available while it is built.',
102                'owner'       => 'jetpack',
103            )
104        );
105    }
106
107    /**
108     * Whether the dashboard's feature flag is on.
109     *
110     * @return bool
111     */
112    public static function is_enabled() {
113        return Feature_Flags::is_enabled( self::FEATURE_FLAG );
114    }
115
116    /**
117     * Wire the hooks.
118     *
119     * @param array $menu_options Admin_Menu item options merged over the defaults, e.g. `module`.
120     * @return void
121     */
122    public static function init( array $menu_options = array() ) {
123        self::$menu_options = $menu_options;
124
125        add_action( 'admin_menu', array( __CLASS__, 'maybe_load_wp_build' ), 1 );
126
127        // A wpcom host adds the item itself: see README.
128        if ( ! ( new Host() )->is_wpcom_platform() ) {
129            // Before Admin_Menu registers its items at 1000.
130            add_action( 'admin_menu', array( __CLASS__, 'add_menu' ), 999 );
131        }
132
133        // Simple reaches WordPress.com directly, so it needs none of the forwarding routes.
134        if ( ! ( new Host() )->is_wpcom_simple() ) {
135            add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
136        }
137
138        add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
139
140        self::register_sections();
141    }
142
143    /**
144     * Register each of SECTIONS, skipping keys already registered so init() can run twice.
145     *
146     * @return void
147     */
148    private static function register_sections() {
149        foreach ( self::SECTIONS as $class ) {
150            $section = new $class();
151            if ( ! isset( self::$sections[ $section->get_key() ] ) ) {
152                self::register_section( $section );
153            }
154        }
155    }
156
157    /**
158     * Add a section to the dashboard.
159     *
160     * @param Dashboard_Section $section The section.
161     * @return void
162     */
163    public static function register_section( Dashboard_Section $section ) {
164        $key = $section->get_key();
165        if ( isset( self::$sections[ $key ] ) ) {
166            /* translators: %s is a dashboard section key. */
167            $message = sprintf( __( 'A Monetize dashboard section with the key "%s" is already registered.', 'jetpack-monetize' ), $key );
168            _doing_it_wrong( __METHOD__, esc_html( $message ), '$$next-version$$' );
169            return;
170        }
171        self::$sections[ $key ] = $section;
172    }
173
174    /**
175     * Register every section's REST routes.
176     *
177     * @return void
178     */
179    public static function register_rest_routes() {
180        foreach ( self::$sections as $section ) {
181            $section->register_routes();
182        }
183    }
184
185    /**
186     * Whether the current user may see the dashboard and use its REST routes.
187     *
188     * @return bool
189     */
190    public static function can_manage() {
191        return current_user_can( 'manage_options' );
192    }
193
194    /**
195     * Each section's state, keyed by section key.
196     *
197     * @return object[]
198     */
199    public static function get_initial_state() {
200        $state = array();
201        foreach ( self::$sections as $key => $section ) {
202            // An empty array would print as `[]`, and the page reads every section's state as an object.
203            $state[ $key ] = (object) $section->get_state();
204        }
205        return $state;
206    }
207
208    /**
209     * Add each section's state to `JetpackScriptData.monetize.sections` on the dashboard's page.
210     *
211     * @param array $data Script data.
212     * @return array
213     */
214    public static function add_script_data( $data ) {
215        if ( self::is_dashboard_request() ) {
216            $data['monetize']['sections'] = (object) self::get_initial_state();
217        }
218        return $data;
219    }
220
221    /**
222     * Load wp-build output, only on this page's request.
223     *
224     * Scoped to the page so WP_Build_Polyfills does not replace core scripts everywhere else.
225     *
226     * @return void
227     */
228    public static function maybe_load_wp_build() {
229        if ( self::is_dashboard_request() ) {
230            self::load_wp_build( dirname( __DIR__ ) . '/build/build.php' );
231        }
232    }
233
234    /**
235     * Whether the request is for the dashboard's page.
236     *
237     * @return bool
238     */
239    public static function is_dashboard_request() {
240        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the page slug only.
241        return isset( $_GET['page'] ) && self::MENU_SLUG === sanitize_text_field( wp_unslash( $_GET['page'] ) );
242    }
243
244    /**
245     * Load a wp-build index and, if it can render the page, its scripts and polyfills.
246     *
247     * @param string $build_index Path to the generated build.php.
248     * @return bool Whether the build can render the page.
249     */
250    public static function load_wp_build( $build_index ) {
251        if ( ! file_exists( $build_index ) ) {
252            return false;
253        }
254
255        $load_wp_build = static function () use ( $build_index ) {
256            require_once $build_index;
257        };
258
259        // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
260        if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
261            WP_Build_Screen_Id::load_with_alias(
262                array( __CLASS__, 'alias_screen_id' ),
263                array( __CLASS__, 'restore_screen_id' ),
264                $load_wp_build
265            );
266        } else {
267            add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id' ) );
268            $load_wp_build();
269            add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id' ) );
270        }
271
272        // A stale or partial build can't render the page, so don't swap core's scripts for it.
273        if ( ! function_exists( self::RENDER_FUNCTION ) ) {
274            return false;
275        }
276
277        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_i18n_loader' ) );
278
279        // wp-build hooks module registration to wp_default_scripts, which has already fired by
280        // admin_menu — call it directly or the init module never reaches the import map.
281        if ( function_exists( 'jetpack_monetize_register_script_modules' ) ) {
282            jetpack_monetize_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
283        }
284
285        WP_Build_Polyfills::register(
286            'jetpack-monetize',
287            array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
288        );
289
290        return true;
291    }
292
293    /**
294     * Point the screen ID at the wp-build page while its generated enqueue check runs.
295     *
296     * @return void
297     */
298    public static function alias_screen_id() {
299        $screen = get_current_screen();
300        if ( ! $screen ) {
301            return;
302        }
303
304        self::$original_screen_id = $screen->id;
305        $screen->id               = self::WP_BUILD_PAGE_ID;
306    }
307
308    /**
309     * Undo alias_screen_id(), since JITM builds its message path from the screen ID.
310     *
311     * @return void
312     */
313    public static function restore_screen_id() {
314        $screen = get_current_screen();
315        if ( ! $screen || null === self::$original_screen_id ) {
316            return;
317        }
318
319        $screen->id               = self::$original_screen_id;
320        self::$original_screen_id = null;
321    }
322
323    /**
324     * Enqueue the JS translation loader, which the esbuild route bundles don't pull in.
325     *
326     * @return void
327     */
328    public static function enqueue_i18n_loader() {
329        if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
330            wp_enqueue_script( 'wp-jp-i18n-loader' );
331        }
332    }
333
334    /**
335     * Add the "Monetize" item to the Jetpack sidebar.
336     *
337     * @return void
338     */
339    public static function add_menu() {
340        Admin_Menu::add_menu(
341            __( 'Monetize', 'jetpack-monetize' ),
342            __( 'Monetize', 'jetpack-monetize' ),
343            'manage_options',
344            self::MENU_SLUG,
345            array( __CLASS__, 'render' ),
346            null,
347            array_merge( array( 'key' => self::MENU_SLUG ), self::$menu_options )
348        );
349    }
350
351    /**
352     * Add the "Monetize" item under the Jetpack menu on Simple, once wpcom-admin-menu.php has built that menu.
353     *
354     * @return void
355     */
356    public static function add_wp_admin_submenu() {
357        add_submenu_page(
358            'jetpack',
359            __( 'Monetize', 'jetpack-monetize' ),
360            __( 'Monetize', 'jetpack-monetize' ),
361            'manage_options',
362            self::MENU_SLUG,
363            array( __CLASS__, 'render' )
364        );
365    }
366
367    /**
368     * Render the wp-build page, or say why it could not be rendered.
369     *
370     * @return void
371     */
372    public static function render() {
373        $render_fn = self::RENDER_FUNCTION;
374        if ( function_exists( $render_fn ) ) {
375            $render_fn();
376            return;
377        }
378
379        printf(
380            '<div class="wrap"><h1>%s</h1><div class="notice notice-error"><p>%s</p></div></div>',
381            esc_html__( 'Monetize', 'jetpack-monetize' ),
382            esc_html__( 'The Monetize dashboard could not be loaded because its assets are missing.', 'jetpack-monetize' )
383        );
384    }
385}