Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
85.71% |
54 / 63 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
| Settings_App | |
85.71% |
54 / 63 |
|
33.33% |
3 / 9 |
29.13 | |
0.00% |
0 / 1 |
| init | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| maybe_load | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
| should_load | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
| load | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
6.07 | |||
| render_callback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
| alias_screen_id | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
| restore_screen_id | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| enqueue_i18n_loader | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| add_script_data | |
100.00% |
18 / 18 |
|
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 | |
| 8 | declare( strict_types = 1 ); |
| 9 | |
| 10 | namespace Automattic\Jetpack\Sharing_Likes\Settings_App; |
| 11 | |
| 12 | use Automattic\Jetpack\Sharing_Likes\REST\Settings_Controller; |
| 13 | use Automattic\Jetpack\Sharing_Likes\REST\Status_Controller; |
| 14 | use Automattic\Jetpack\Sharing_Likes\Settings\Placement_Section; |
| 15 | use Automattic\Jetpack\Sharing_Likes\Settings\Settings_Page; |
| 16 | use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills; |
| 17 | use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id; |
| 18 | use 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 | */ |
| 25 | final 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 | } |