Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
73.49% |
61 / 83 |
|
58.82% |
10 / 17 |
CRAP | |
0.00% |
0 / 1 |
| WPCOM_WP_Build_Page | |
74.39% |
61 / 82 |
|
58.82% |
10 / 17 |
81.54 | |
0.00% |
0 / 1 |
| get_page_title | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| get_menu_title | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| get_initial_state | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| should_register | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| init | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
5.40 | |||
| build_dir | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| has_build | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_routes | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
6.07 | |||
| get_render_callback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_page_url | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_page_request | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
4.59 | |||
| register_page | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 | |||
| owns_page | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_screen_id | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| reset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| slug_is_claimed | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| load_wp_build | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
| alias_screen_id | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| restore_screen_id | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| enqueue_page_data | |
73.33% |
11 / 15 |
|
0.00% |
0 / 1 |
10.54 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Shared plumbing for mu-wpcom's wp-build admin pages. |
| 4 | * |
| 5 | * @package automattic/jetpack-mu-wpcom |
| 6 | */ |
| 7 | |
| 8 | // @phan-file-suppress PhanAbstractStaticMethodCallInStatic -- static:: dispatches to the concrete page; the base is never booted itself. |
| 9 | |
| 10 | namespace Automattic\Jetpack\Jetpack_Mu_Wpcom; |
| 11 | |
| 12 | use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills; |
| 13 | |
| 14 | require_once __DIR__ . '/wp-build-script-translations.php'; |
| 15 | |
| 16 | /** |
| 17 | * Serves a wp-build page under a wp-admin menu slug of the subclass's choosing. |
| 18 | * |
| 19 | * Subclasses are static and boot with `add_action( 'init', array( Subclass::class, 'init' ) )`. |
| 20 | */ |
| 21 | abstract class WPCOM_WP_Build_Page { |
| 22 | |
| 23 | /** |
| 24 | * Admin page slug, which stays in the page URL. |
| 25 | */ |
| 26 | const MENU_SLUG = ''; |
| 27 | |
| 28 | /** |
| 29 | * Page name for wp-build, as declared in the page's routes/<route>/package.json. |
| 30 | * |
| 31 | * Must differ from the menu slug, or wp-build's generated standalone page takes the slug over. |
| 32 | */ |
| 33 | const WP_BUILD_PAGE = ''; |
| 34 | |
| 35 | /** |
| 36 | * Menu the page sits under. |
| 37 | */ |
| 38 | const PARENT_SLUG = 'jetpack'; |
| 39 | |
| 40 | /** |
| 41 | * Capability for the page, and for receiving its initial state. |
| 42 | */ |
| 43 | const CAPABILITY = 'manage_options'; |
| 44 | |
| 45 | /** |
| 46 | * `admin_menu` priority for register_page(). |
| 47 | * |
| 48 | * After Jetpack's Admin_Menu (1000), so a slug it claimed is visible, and before |
| 49 | * wpcom_add_jetpack_submenu() (999999), which reads whether this page owns its slug. |
| 50 | */ |
| 51 | const REGISTER_PRIORITY = 999998; |
| 52 | |
| 53 | /** |
| 54 | * Global the page's boot script reads get_initial_state() from. |
| 55 | */ |
| 56 | const INITIAL_STATE_VAR = ''; |
| 57 | |
| 58 | /** |
| 59 | * Whether the page loads WordPress.com Tracks. |
| 60 | */ |
| 61 | const ENQUEUE_TRACKS = false; |
| 62 | |
| 63 | /** |
| 64 | * Whether each page registered its slug on this request, keyed by class. |
| 65 | * |
| 66 | * @var bool[] |
| 67 | */ |
| 68 | private static $owns_page = array(); |
| 69 | |
| 70 | /** |
| 71 | * The screen ID displaced by alias_screen_id(), keyed by class. |
| 72 | * |
| 73 | * @var string[] |
| 74 | */ |
| 75 | private static $displaced_screen_id = array(); |
| 76 | |
| 77 | /** |
| 78 | * The hook suffix add_submenu_page() returned, which is the page's screen ID, keyed by class. |
| 79 | * |
| 80 | * @var string[] |
| 81 | */ |
| 82 | private static $screen_id = array(); |
| 83 | |
| 84 | /** |
| 85 | * The page title. |
| 86 | * |
| 87 | * @return string |
| 88 | */ |
| 89 | abstract public static function get_page_title(); |
| 90 | |
| 91 | /** |
| 92 | * The menu entry's label. |
| 93 | * |
| 94 | * @return string |
| 95 | */ |
| 96 | abstract public static function get_menu_title(); |
| 97 | |
| 98 | /** |
| 99 | * The state the page's boot script reads from INITIAL_STATE_VAR. |
| 100 | * |
| 101 | * @return array |
| 102 | */ |
| 103 | abstract public static function get_initial_state(); |
| 104 | |
| 105 | /** |
| 106 | * Whether this page should claim its slug on this site. |
| 107 | * |
| 108 | * @return bool |
| 109 | */ |
| 110 | public static function should_register() { |
| 111 | return true; |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Boot the page, loading the wp-build assets only on its own request. |
| 116 | * |
| 117 | * @return void |
| 118 | */ |
| 119 | public static function init() { |
| 120 | // Without a build the page would render blank, so leave the slug to whatever serves it otherwise. |
| 121 | if ( ! static::should_register() || ! static::has_build() ) { |
| 122 | return; |
| 123 | } |
| 124 | |
| 125 | add_action( 'admin_menu', array( static::class, 'register_page' ), static::REGISTER_PRIORITY ); |
| 126 | |
| 127 | if ( ! static::is_page_request() ) { |
| 128 | return; |
| 129 | } |
| 130 | |
| 131 | static::load_wp_build(); |
| 132 | |
| 133 | // wp-build's generated enqueue runs at the default priority 10; the alias is |
| 134 | // scoped to that one pass so nothing else sees the rewritten screen. |
| 135 | add_action( 'admin_enqueue_scripts', array( static::class, 'alias_screen_id' ), 9 ); |
| 136 | add_action( 'admin_enqueue_scripts', array( static::class, 'restore_screen_id' ), 11 ); |
| 137 | add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_page_data' ), 20 ); |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * The wp-build output directory, produced at release time and absent from an unbuilt checkout. |
| 142 | * |
| 143 | * @return string |
| 144 | */ |
| 145 | public static function build_dir() { |
| 146 | return dirname( __DIR__, 2 ) . '/build'; |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Whether the wp-build output the page renders from exists. |
| 151 | * |
| 152 | * @return bool |
| 153 | */ |
| 154 | public static function has_build() { |
| 155 | return file_exists( static::build_dir() . '/build.php' ); |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * The page's wp-build route directories, read from the build's route registry. |
| 160 | * |
| 161 | * @return string[] |
| 162 | */ |
| 163 | public static function get_routes() { |
| 164 | $registry = static::build_dir() . '/routes/registry.php'; |
| 165 | |
| 166 | if ( ! file_exists( $registry ) ) { |
| 167 | return array(); |
| 168 | } |
| 169 | |
| 170 | $routes = array(); |
| 171 | foreach ( (array) require $registry as $route ) { |
| 172 | if ( is_array( $route ) && static::WP_BUILD_PAGE === ( $route['page'] ?? null ) && isset( $route['name'] ) ) { |
| 173 | $routes[] = $route['name']; |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | return $routes; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Render callback wp-build generates in build/pages/<page>/page-wp-admin.php. |
| 182 | * |
| 183 | * @return string |
| 184 | */ |
| 185 | public static function get_render_callback() { |
| 186 | return 'jetpack_mu_wpcom_' . str_replace( '-', '_', static::WP_BUILD_PAGE ) . '_wp_admin_render_page'; |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * The page's own address. |
| 191 | * |
| 192 | * @return string |
| 193 | */ |
| 194 | public static function get_page_url() { |
| 195 | return admin_url( 'admin.php?page=' . static::MENU_SLUG ); |
| 196 | } |
| 197 | |
| 198 | /** |
| 199 | * Whether the current request targets this page. |
| 200 | * |
| 201 | * @return bool |
| 202 | */ |
| 203 | public static function is_page_request() { |
| 204 | // admin-ajax.php is also `is_admin()`, and has no menu, screen or enqueue pass to set up. |
| 205 | if ( ! is_admin() || 'admin.php' !== ( $GLOBALS['pagenow'] ?? '' ) ) { |
| 206 | return false; |
| 207 | } |
| 208 | |
| 209 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing check; no form data is processed. |
| 210 | return isset( $_GET['page'] ) && static::MENU_SLUG === sanitize_text_field( wp_unslash( $_GET['page'] ) ); |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * Register the page, unless something else already serves the slug. |
| 215 | * |
| 216 | * @return void |
| 217 | */ |
| 218 | public static function register_page() { |
| 219 | // A second entry under a slug someone else owns would leave two. |
| 220 | if ( static::slug_is_claimed() ) { |
| 221 | return; |
| 222 | } |
| 223 | |
| 224 | self::$owns_page[ static::class ] = true; |
| 225 | |
| 226 | // build/build.php defines the render callback but only loads on this page's |
| 227 | // own request; elsewhere the callback never runs, so any callable will do. |
| 228 | $callback = function_exists( static::get_render_callback() ) |
| 229 | ? static::get_render_callback() |
| 230 | : '__return_empty_string'; |
| 231 | |
| 232 | $hook = add_submenu_page( |
| 233 | static::PARENT_SLUG, |
| 234 | static::get_page_title(), |
| 235 | static::get_menu_title(), |
| 236 | static::CAPABILITY, |
| 237 | static::MENU_SLUG, |
| 238 | $callback |
| 239 | ); |
| 240 | |
| 241 | if ( $hook ) { |
| 242 | self::$screen_id[ static::class ] = $hook; |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Whether this page registered its slug on this request. |
| 248 | * |
| 249 | * @return bool |
| 250 | */ |
| 251 | public static function owns_page() { |
| 252 | return ! empty( self::$owns_page[ static::class ] ); |
| 253 | } |
| 254 | |
| 255 | /** |
| 256 | * The page's screen ID, or null when it did not register on this request. |
| 257 | * |
| 258 | * @return string|null |
| 259 | */ |
| 260 | public static function get_screen_id() { |
| 261 | return self::$screen_id[ static::class ] ?? null; |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Reset the registration state. Test seam; production sets this once per request. |
| 266 | * |
| 267 | * @return void |
| 268 | */ |
| 269 | public static function reset() { |
| 270 | unset( self::$owns_page[ static::class ], self::$displaced_screen_id[ static::class ], self::$screen_id[ static::class ] ); |
| 271 | } |
| 272 | |
| 273 | /** |
| 274 | * Whether something has already registered this page's slug. |
| 275 | * |
| 276 | * @return bool |
| 277 | */ |
| 278 | public static function slug_is_claimed() { |
| 279 | global $submenu; |
| 280 | |
| 281 | foreach ( $submenu[ static::PARENT_SLUG ] ?? array() as $item ) { |
| 282 | if ( is_array( $item ) && static::MENU_SLUG === ( $item[2] ?? null ) ) { |
| 283 | return true; |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | return false; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * Load the wp-build generated asset registrations and the boot polyfills. |
| 292 | * |
| 293 | * @return void |
| 294 | */ |
| 295 | public static function load_wp_build() { |
| 296 | $wp_build_index = static::build_dir() . '/build.php'; |
| 297 | require_once $wp_build_index; |
| 298 | |
| 299 | // Register polyfills for WP < 7.0 (must run before enqueue). |
| 300 | WP_Build_Polyfills::register( |
| 301 | 'jetpack-mu-wpcom', |
| 302 | array_merge( |
| 303 | WP_Build_Polyfills::SCRIPT_HANDLES, |
| 304 | WP_Build_Polyfills::MODULE_IDS |
| 305 | ) |
| 306 | ); |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Make wp-build's enqueue check, which wants a screen ID of WP_BUILD_PAGE, pass without changing the URL. |
| 311 | * |
| 312 | * Paired with restore_screen_id() so the body class and page-view tracker keep reading the real ID. |
| 313 | * |
| 314 | * @return void |
| 315 | */ |
| 316 | public static function alias_screen_id() { |
| 317 | $screen = get_current_screen(); |
| 318 | |
| 319 | if ( ! static::owns_page() || ! is_object( $screen ) ) { |
| 320 | return; |
| 321 | } |
| 322 | |
| 323 | self::$displaced_screen_id[ static::class ] = $screen->id; |
| 324 | $screen->id = static::WP_BUILD_PAGE; |
| 325 | } |
| 326 | |
| 327 | /** |
| 328 | * Put back the screen ID displaced by alias_screen_id(). |
| 329 | * |
| 330 | * @return void |
| 331 | */ |
| 332 | public static function restore_screen_id() { |
| 333 | $screen = get_current_screen(); |
| 334 | |
| 335 | if ( ! isset( self::$displaced_screen_id[ static::class ] ) || ! is_object( $screen ) ) { |
| 336 | return; |
| 337 | } |
| 338 | |
| 339 | $screen->id = self::$displaced_screen_id[ static::class ]; |
| 340 | unset( self::$displaced_screen_id[ static::class ] ); |
| 341 | } |
| 342 | |
| 343 | /** |
| 344 | * Hand the page its initial state, Tracks and translation catalogs. |
| 345 | * |
| 346 | * All of it lands on the prerequisites script, which runs before the boot module. |
| 347 | * |
| 348 | * @return void |
| 349 | */ |
| 350 | public static function enqueue_page_data() { |
| 351 | $handle = static::WP_BUILD_PAGE . '-wp-admin-prerequisites'; |
| 352 | |
| 353 | // The menu's capability gate already keeps others off the page; restated because |
| 354 | // subclasses build the state from direct calls rather than gated endpoints. |
| 355 | if ( ! static::owns_page() || ! wp_script_is( $handle, 'registered' ) || ! current_user_can( static::CAPABILITY ) ) { |
| 356 | return; |
| 357 | } |
| 358 | |
| 359 | if ( '' !== static::INITIAL_STATE_VAR ) { |
| 360 | // Inline JSON rather than wp_localize_script(), which stringifies top-level scalars. |
| 361 | $state = wp_json_encode( static::get_initial_state(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ); |
| 362 | |
| 363 | // Every `before` script shares one tag, so a bare `= ;` would also break the translations. |
| 364 | if ( false !== $state ) { |
| 365 | wp_add_inline_script( $handle, 'window.' . static::INITIAL_STATE_VAR . ' = ' . $state . ';', 'before' ); |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | if ( static::ENQUEUE_TRACKS ) { |
| 370 | Common\wpcom_enqueue_tracking_scripts( $handle ); |
| 371 | } |
| 372 | |
| 373 | $constants = static::build_dir() . '/constants.php'; |
| 374 | if ( file_exists( $constants ) ) { |
| 375 | $build_constants = require $constants; |
| 376 | $translations = wpcom_wp_build_script_translations( static::build_dir() . '/i18n-manifest.json', $build_constants['build_url'], static::get_routes() ); |
| 377 | |
| 378 | if ( null !== $translations ) { |
| 379 | wp_add_inline_script( $handle, $translations, 'before' ); |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | } |