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