Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.94% covered (warning)
85.94%
55 / 64
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Settings_App
85.94% covered (warning)
85.94%
55 / 64
33.33% covered (danger)
33.33%
3 / 9
29.03
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_load
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 should_load
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 load
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
6.07
 render_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 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
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 add_script_data
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Loads the React version of Settings > Sharing.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes\Settings_App;
11
12use Automattic\Jetpack\Sharing_Likes\REST\Settings_Controller;
13use Automattic\Jetpack\Sharing_Likes\REST\Status_Controller;
14use Automattic\Jetpack\Sharing_Likes\Settings\Placement_Section;
15use Automattic\Jetpack\Sharing_Likes\Settings\Settings_Page;
16use Automattic\Jetpack\Status;
17use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
18use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
19use WP_REST_Request;
20
21/**
22 * Swaps the PHP screen for the wp-build app on sites that opt in.
23 *
24 * Everything here runs on the Sharing page only: the polyfills force-replace core script handles.
25 */
26final class Settings_App {
27
28    /**
29     * Opt-in filter. Off by default while the React screen is being built.
30     */
31    public const FILTER = 'rsm_jetpack_ui_modernization_sharing_likes';
32
33    /**
34     * The wp-build page ID, which wp-build's generated enqueue check compares the screen ID against.
35     */
36    public const WP_BUILD_PAGE = 'jetpack-sharing-settings';
37
38    private const RENDER_FUNCTION    = 'jetpack_sharing_likes_jetpack_sharing_settings_wp_admin_render_page';
39    private const MODULES_FUNCTION   = 'jetpack_sharing_likes_register_script_modules';
40    private const INTERCEPT_FUNCTION = 'jetpack_sharing_likes_jetpack_sharing_settings_intercept_render';
41
42    /**
43     * Whether `load()` loaded the build in this request.
44     *
45     * @var bool
46     */
47    private static $loaded = false;
48
49    /**
50     * The real screen ID while it is aliased.
51     *
52     * @var string|null
53     */
54    private static $original_screen_id = null;
55
56    /**
57     * Hook the loader.
58     */
59    public static function init(): void {
60        // Priority 1: opt-in code adds the filter on `plugins_loaded`, and the menu registers at 10.
61        add_action( 'admin_menu', array( __CLASS__, 'maybe_load' ), 1 );
62    }
63
64    /**
65     * Load the app when this request is for it.
66     */
67    public static function maybe_load(): void {
68        if ( self::should_load() ) {
69            self::load( dirname( __DIR__, 2 ) . '/build/build.php' );
70        }
71    }
72
73    /**
74     * Whether this is a wp-admin request for the Sharing page on a site that opted in.
75     *
76     * Not true for Calypso's `wpcom/v2/admin-menu`, which fires `admin_menu` in a REST request.
77     */
78    public static function should_load(): bool {
79        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only decides which screen to render.
80        if ( ! is_admin() || ! isset( $_GET['page'] ) || ! is_string( $_GET['page'] ) ) {
81            return false;
82        }
83
84        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only decides which screen to render.
85        if ( Settings_Page::SLUG !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) {
86            return false;
87        }
88
89        return (bool) apply_filters( self::FILTER, false );
90    }
91
92    /**
93     * Load the generated build and everything the page needs around it.
94     *
95     * Public so tests can point it at a fixture; callers go through `maybe_load()`.
96     *
97     * @param string $build_index Path to wp-build's `build.php`.
98     */
99    public static function load( string $build_index ): void {
100        if ( self::$loaded || ! file_exists( $build_index ) ) {
101            return;
102        }
103
104        $require = static function () use ( $build_index ): void {
105            require_once $build_index;
106        };
107
108        // An older wp-build-polyfills under the Jetpack autoloader may predate load_with_alias().
109        if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
110            WP_Build_Screen_Id::load_with_alias( array( __CLASS__, 'alias_screen_id' ), array( __CLASS__, 'restore_screen_id' ), $require );
111        } else {
112            add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id' ) );
113            $require();
114            add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id' ) );
115        }
116
117        // A stale or partial build can't render the page, so don't swap core's scripts for it.
118        if ( ! function_exists( self::RENDER_FUNCTION ) ) {
119            remove_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id' ) );
120            remove_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id' ) );
121            return;
122        }
123
124        // The generated file registers these on `wp_default_scripts`, which has already fired by `admin_menu`.
125        if ( function_exists( self::MODULES_FUNCTION ) ) {
126            call_user_func( self::MODULES_FUNCTION );
127        }
128
129        remove_action( 'admin_init', self::INTERCEPT_FUNCTION );
130
131        WP_Build_Polyfills::register(
132            'jetpack-sharing-likes',
133            array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
134        );
135
136        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_i18n_loader' ) );
137        add_filter( 'jetpack_admin_js_script_data', array( __CLASS__, 'add_script_data' ) );
138
139        self::$loaded = true;
140    }
141
142    /**
143     * The generated render function, once the build loaded.
144     */
145    public static function render_callback(): ?callable {
146        return self::$loaded && function_exists( self::RENDER_FUNCTION ) ? self::RENDER_FUNCTION : null;
147    }
148
149    /**
150     * Show wp-build's generated enqueue check the page ID it expects.
151     */
152    public static function alias_screen_id(): void {
153        $screen = get_current_screen();
154        if ( ! $screen ) {
155            return;
156        }
157
158        self::$original_screen_id = $screen->id;
159        $screen->id               = self::WP_BUILD_PAGE;
160    }
161
162    /**
163     * Put the real screen ID back for everything after the generated check.
164     */
165    public static function restore_screen_id(): void {
166        $screen = get_current_screen();
167        if ( ! $screen || null === self::$original_screen_id ) {
168            return;
169        }
170
171        $screen->id               = self::$original_screen_id;
172        self::$original_screen_id = null;
173    }
174
175    /**
176     * Jetpack's i18n loader is registered everywhere but only enqueued on demand; the init module needs it.
177     */
178    public static function enqueue_i18n_loader(): void {
179        if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
180            wp_enqueue_script( 'wp-jp-i18n-loader' );
181        }
182    }
183
184    /**
185     * Hand the first render what the routes would answer, plus what only PHP knows.
186     *
187     * @param array $data Script data.
188     * @return array
189     */
190    public static function add_script_data( $data ) {
191        $data = is_array( $data ) ? $data : array();
192
193        if ( ! current_user_can( 'manage_options' ) ) {
194            return $data;
195        }
196
197        $data['sharing_likes'] = array(
198            'status'              => ( new Status_Controller() )->get_status()->get_data(),
199            // An empty list would encode as `[]`.
200            'settings'            => (object) ( new Settings_Controller() )->get_item( new WP_REST_Request() )->get_data(),
201            'placement_choices'   => array_map(
202                static function ( string $choice ): array {
203                    return array(
204                        'value' => $choice,
205                        'label' => Placement_Section::label_for( $choice ),
206                    );
207                },
208                Placement_Section::choices()
209            ),
210            'multibyte_supported' => function_exists( 'mb_stripos' ),
211            'private_site'        => ( new Status() )->is_private_site(),
212        );
213
214        return $data;
215    }
216}