Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.63% covered (warning)
69.63%
94 / 135
52.38% covered (warning)
52.38%
11 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Activity_Log
69.92% covered (warning)
69.92%
93 / 133
52.38% covered (warning)
52.38%
11 / 21
133.33
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 is_module_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register_module
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 add_standalone_module
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 activate_standalone_default
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 add_wp_admin_submenu
89.29% covered (warning)
89.29%
25 / 28
0.00% covered (danger)
0.00%
0 / 1
6.04
 add_admin_shell_member
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 load_as_admin_shell_guest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_available
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 admin_init
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 load_wp_build
23.08% covered (danger)
23.08%
3 / 13
0.00% covered (danger)
0.00%
0 / 1
7.10
 load_wp_build_with_screen_alias
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
2.06
 alias_screen_id_for_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 restore_screen_id_after_wp_build
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 opt_out_of_jitms
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hide_jitms_on_wp_build_dashboard
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 enqueue_initial_state
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 is_wpcom_simple
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render_fallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_activity_log_admin_request
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 register_rest_routes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Primary class for the Jetpack Activity Log package.
4 *
5 * @package automattic/jetpack-activity-log
6 */
7
8namespace Automattic\Jetpack\Activity_Log;
9
10if ( ! defined( 'ABSPATH' ) ) {
11    exit( 0 );
12}
13
14use Automattic\Jetpack\Activity_Log\Initial_State as Activity_Log_Initial_State;
15use Automattic\Jetpack\Admin_UI\Admin_Menu;
16use Automattic\Jetpack\Admin_UI\Admin_Shell;
17use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
18use Automattic\Jetpack\Connection\Manager as Connection_Manager;
19use Automattic\Jetpack\Modules;
20use Automattic\Jetpack\Status\Host;
21use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
22use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Screen_Id;
23use Jetpack_Options;
24use function add_action;
25use function add_filter;
26use function add_submenu_page;
27use function class_exists;
28use function current_user_can;
29use function did_action;
30use function do_action;
31use function get_current_screen;
32use function is_admin;
33use function is_multisite;
34use function sanitize_text_field;
35use function wp_add_inline_script;
36use function wp_enqueue_script;
37use function wp_json_encode;
38use function wp_register_script;
39use function wp_unslash;
40use function wp_verify_nonce;
41
42/**
43 * Class Jetpack_Activity_Log
44 *
45 * Registers the Activity Log admin page and its REST routes inside the
46 * main Jetpack plugin.
47 */
48class Jetpack_Activity_Log {
49
50    /**
51     * Admin page slug.
52     *
53     * @var string
54     */
55    const PAGE_SLUG = 'jetpack-activity-log';
56
57    /**
58     * Slug of the Jetpack module that turns the Activity Log on and off.
59     *
60     * @var string
61     */
62    const MODULE_SLUG = 'activity-log';
63
64    /**
65     * Jetpack_Options key recording that the module was switched on for a site
66     * with no Jetpack plugin. See `activate_standalone_default()`.
67     *
68     * @var string
69     */
70    const DEFAULT_ACTIVATED_OPTION = 'activity_log_default_activated';
71
72    /**
73     * Page slug for the wp-build dashboard. Distinct from the wp-admin menu
74     * slug (`PAGE_SLUG`) so the user-facing URL stays `admin.php?page=jetpack-activity-log`;
75     * we alias the current screen id to this value so wp-build's
76     * screen-match enqueue callback fires. Must match the `page` in
77     * `routes/dashboard/package.json` and the `wpPlugin.pages` entry.
78     *
79     * @var string
80     */
81    const WP_BUILD_PAGE_SLUG = 'jetpack-activity-log-dashboard';
82
83    /**
84     * Handle for the classic script that carries the React initial state.
85     * The dashboard is a wp-build script module, so there is no classic
86     * bundle handle to attach inline data to — this empty handle exists
87     * purely to print `JPACTIVITYLOG_INITIAL_STATE` and the Connection
88     * initial state before boot runs.
89     *
90     * @var string
91     */
92    const DATA_SCRIPT_HANDLE = 'jetpack-activity-log-data';
93
94    /**
95     * Nonce action for refreshing the access flag after a checkout
96     * return. Used by `admin_init()` below and exposed to the client via
97     * Initial_State so the upsell CTA can embed a valid nonce in its
98     * `redirect_to`. Same shape as `Social_Admin_Page::REFRESH_PLAN_NONCE_ACTION`.
99     *
100     * @var string
101     */
102    const REFRESH_ACCESS_NONCE_ACTION = 'jetpack_activity_log_refresh_access';
103
104    /**
105     * The screen ID alias_screen_id_for_wp_build() replaced, until it is restored.
106     *
107     * @var string|null
108     */
109    private static $wp_build_original_screen_id = null;
110
111    /**
112     * The dashboard screen hide_jitms_on_wp_build_dashboard() opts out of JITMs.
113     *
114     * @var string|null
115     */
116    private static $jitm_opt_out_screen_id = null;
117
118    /**
119     * Entry point. Idempotent: safe to call from multiple bootstraps.
120     *
121     * Bootstraps only while the `activity-log` module is on, so the toggle
122     * means the same thing to the Jetpack plugin and to every standalone
123     * plugin that carries this package.
124     */
125    public static function initialize() {
126        // Simple has no module system, and jetpack-mu-wpcom registers the page itself via add_wp_admin_submenu().
127        if ( self::is_wpcom_simple() ) {
128            return;
129        }
130
131        self::register_module();
132
133        if ( did_action( 'jetpack_activity_log_initialized' ) || ! self::is_module_active() ) {
134            return;
135        }
136
137        add_action( 'admin_menu', array( __CLASS__, 'add_wp_admin_submenu' ) );
138        add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
139        add_filter( 'jetpack_package_versions', array( Package_Version::class, 'send_package_version_to_tracker' ) );
140
141        if ( class_exists( Admin_Shell::class ) ) {
142            add_filter( 'jetpack_admin_shell_members', array( __CLASS__, 'add_admin_shell_member' ) );
143        }
144
145        /**
146         * Fires once the Jetpack Activity Log package has wired its hooks.
147         *
148         * @since 0.1.0
149         */
150        do_action( 'jetpack_activity_log_initialized' );
151    }
152
153    /**
154     * Whether the Activity Log module is switched on.
155     *
156     * @return bool
157     */
158    public static function is_module_active() {
159        return ( new Modules() )->is_active( self::MODULE_SLUG );
160    }
161
162    /**
163     * Make the module controllable, and give a site with no Jetpack plugin the
164     * same default-on state the Jetpack plugin gets from `Auto Activate: Yes`.
165     *
166     * @return void
167     */
168    private static function register_module() {
169        add_filter( 'jetpack_get_available_standalone_modules', array( __CLASS__, 'add_standalone_module' ) );
170
171        // `class_exists( 'Jetpack' )` is only reliable once every plugin file has
172        // loaded: `jetpack-backup/` sorts before `jetpack/` in active_plugins, so
173        // Backup reaches initialize() while the Jetpack class is still undefined.
174        if ( did_action( 'plugins_loaded' ) ) {
175            self::activate_standalone_default();
176        } else {
177            add_action( 'plugins_loaded', array( __CLASS__, 'activate_standalone_default' ) );
178        }
179    }
180
181    /**
182     * Make the module available to the module controller when the Jetpack
183     * plugin is not installed, so `jetpack_active_modules` is not inert there.
184     *
185     * @param array $modules Available standalone module slugs.
186     * @return array
187     */
188    public static function add_standalone_module( $modules ) {
189        $modules[] = self::MODULE_SLUG;
190
191        return array_values( array_unique( $modules ) );
192    }
193
194    /**
195     * Switch the module on once on a site with no Jetpack plugin.
196     *
197     * The Jetpack plugin activates the module for you via `Auto Activate: Yes`;
198     * a standalone install has no equivalent, so without this the page would
199     * disappear from every Backup/Boost/Protect/Search/VideoPress site on
200     * upgrade. Recorded in an option rather than repeated, so a later opt-out
201     * is not undone on the next request.
202     *
203     * @return void
204     */
205    public static function activate_standalone_default() {
206        if ( class_exists( 'Jetpack' ) || Jetpack_Options::get_option( self::DEFAULT_ACTIVATED_OPTION ) ) {
207            return;
208        }
209
210        if ( ! ( new Modules() )->activate( self::MODULE_SLUG, false, false ) ) {
211            return;
212        }
213
214        // Record before re-entering initialize(), which calls back into here.
215        Jetpack_Options::update_option( self::DEFAULT_ACTIVATED_OPTION, true );
216
217        // initialize() ran before this and found the module off, so wire up now
218        // rather than leaving the page missing for the rest of the request.
219        self::initialize();
220    }
221
222    /**
223     * Register the Activity Log submenu under Jetpack.
224     *
225     * Mirrors the gating used by the legacy my-jetpack "Activity Log" menu
226     * item (connected user + non-multisite). On WordPress.com Simple sites,
227     * jetpack-mu-wpcom calls this once it has built the Jetpack menu.
228     *
229     * @since 0.6.0 Registers the page on WordPress.com Simple sites.
230     *
231     * @return string|null The resulting page's hook suffix, if registered.
232     */
233    public static function add_wp_admin_submenu() {
234        if ( ! self::is_available() ) {
235            return null;
236        }
237
238        // Load wp-build only on the Activity Log request so its generated
239        // render function exists before the menu callback runs, and its
240        // enqueue pipeline/polyfills stay off every other admin page.
241        if ( self::is_activity_log_admin_request() ) {
242            self::load_wp_build_with_screen_alias();
243        }
244
245        // The menu item must appear on every admin page, but the generated
246        // render function is only loaded on the Activity Log request (above).
247        // The callback is only ever invoked while rendering our page — where
248        // the function is loaded — so the fallback is purely defensive.
249        $render_callback = function_exists( 'jetpack_activity_log_jetpack_activity_log_dashboard_wp_admin_render_page' )
250            ? 'jetpack_activity_log_jetpack_activity_log_dashboard_wp_admin_render_page'
251            : array( __CLASS__, 'render_fallback' );
252
253        if ( self::is_wpcom_simple() ) {
254            // jetpack-mu-wpcom calls this after Admin_Menu has already flushed its queue.
255            $page_suffix = add_submenu_page(
256                'jetpack',
257                /** "Activity Log" is a product name, do not translate. */
258                'Activity Log',
259                'Activity Log',
260                'manage_options',
261                self::PAGE_SLUG,
262                $render_callback
263            );
264        } else {
265            $page_suffix = Admin_Menu::add_menu(
266                /** "Activity Log" is a product name, do not translate. */
267                'Activity Log',
268                'Activity Log',
269                'manage_options',
270                self::PAGE_SLUG,
271                $render_callback
272            );
273        }
274
275        if ( ! $page_suffix ) {
276            return null;
277        }
278
279        add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
280        self::opt_out_of_jitms( $page_suffix );
281
282        return $page_suffix;
283    }
284
285    /**
286     * Register the Activity Log with the admin shell, which switches between Jetpack dashboards without a page load.
287     *
288     * @since $$next-version$$
289     *
290     * @param array $members Shell members keyed by admin page slug.
291     * @return array
292     */
293    public static function add_admin_shell_member( $members ) {
294        if ( ! self::is_available() || ! file_exists( dirname( __DIR__ ) . '/build/build.php' ) ) {
295            return $members;
296        }
297
298        $members[ self::PAGE_SLUG ] = array(
299            'build_prefix' => 'jetpack_activity_log',
300            'build_page'   => self::WP_BUILD_PAGE_SLUG,
301            'init_modules' => array( '@jetpack-activity-log/init' ),
302            /** "Activity Log" is a product name, do not translate. */
303            'title'        => 'Activity Log',
304            'capability'   => 'manage_options',
305            'handles'      => array( self::DATA_SCRIPT_HANDLE, 'jetpack-script-data', 'jp-tracks', 'wp-jp-i18n-loader' ),
306            'load'         => array( __CLASS__, 'load_as_admin_shell_guest' ),
307        );
308        return $members;
309    }
310
311    /**
312     * Ready the Activity Log routes and page data while another dashboard's page is open, without the screen ID alias.
313     *
314     * @since $$next-version$$
315     *
316     * @return void
317     */
318    public static function load_as_admin_shell_guest() {
319        self::load_wp_build();
320        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_initial_state' ) );
321    }
322
323    /**
324     * Whether the Activity Log page should be shown to the current user.
325     *
326     * @return bool
327     */
328    public static function is_available() {
329        if ( ! current_user_can( 'manage_options' ) ) {
330            return false;
331        }
332
333        // Simple sites need no Jetpack connection: their users are WordPress.com users.
334        if ( self::is_wpcom_simple() ) {
335            return true;
336        }
337
338        if ( is_multisite() ) {
339            return false;
340        }
341
342        return ( new Connection_Manager() )->is_user_connected();
343    }
344
345    /**
346     * Fires when the admin page is loaded.
347     *
348     * When the user is returning from a successful checkout, the upsell
349     * CTA appends `?refresh_access=1&_wpnonce=…` to the `redirect_to`
350     * value it hands off to WordPress.com. Detect that here, verify the
351     * nonce, and drop the cached paid-plan signal so
352     * `Initial_State::get_data()` (which runs later in the same request,
353     * when the bundle is enqueued) rehydrates from WPCOM instead of
354     * re-serving the pre-checkout value. Mirrors the pattern in
355     * `Automattic\Jetpack\Publicize\Social_Admin_Page::admin_init()`.
356     */
357    public static function admin_init() {
358        if ( isset( $_GET['refresh_access'] ) && isset( $_GET['_wpnonce'] ) ) {
359            $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) );
360            if ( wp_verify_nonce( $nonce, self::REFRESH_ACCESS_NONCE_ACTION ) ) {
361                REST_Controller::clear_access_cache();
362            }
363        }
364
365        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_initial_state' ) );
366    }
367
368    /**
369     * Require the generated wp-build entry and register the script/module
370     * polyfills the boot bundle depends on.
371     *
372     * The boot bundle depends on `@wordpress/*` handles (e.g. `wp-theme`,
373     * pulled in via `@wordpress/ui`) that Core does not register on older
374     * WordPress versions. Without them WP_Scripts silently drops the bundle
375     * and the page renders blank, so register the polyfills here. Scoped to
376     * the Activity Log request by the sole caller, since the register() call
377     * can force-replace Core handles and must not fire on every admin page.
378     *
379     * @return void
380     */
381    private static function load_wp_build() {
382        $build_index = dirname( __DIR__ ) . '/build/build.php';
383
384        if ( ! file_exists( $build_index ) ) {
385            return;
386        }
387
388        require_once $build_index;
389
390        // The generated `modules.php` registers standalone script modules (the
391        // `@jetpack-activity-log/init` i18n bootstrap) on `wp_default_scripts`.
392        // We load wp-build lazily on `admin_menu`, which can run after that
393        // action has already fired — so the hook may be added too late and the
394        // init module never registers, leaving it out of the import map and
395        // breaking boot. Register directly here (mirroring the polyfills call
396        // below); the generated function guards against double-registration.
397        if ( function_exists( 'jetpack_activity_log_register_script_modules' ) ) {
398            jetpack_activity_log_register_script_modules(); // @phan-suppress-current-line PhanUndeclaredFunction -- Checked with function_exists(); defined in the generated build/modules.php, which Phan excludes.
399        }
400
401        WP_Build_Polyfills::register(
402            'jetpack-activity-log',
403            array_merge(
404                WP_Build_Polyfills::SCRIPT_HANDLES,
405                WP_Build_Polyfills::MODULE_IDS
406            )
407        );
408    }
409
410    /**
411     * Load wp-build with the screen ID aliased across its generated enqueue check.
412     *
413     * @see WP_Build_Screen_Id::load_with_alias()
414     * @return void
415     */
416    private static function load_wp_build_with_screen_alias() {
417        // Fallback: an older wp-build-polyfills under the jetpack-autoloader may predate load_with_alias().
418        if ( method_exists( WP_Build_Screen_Id::class, 'load_with_alias' ) ) {
419            WP_Build_Screen_Id::load_with_alias(
420                array( __CLASS__, 'alias_screen_id_for_wp_build' ),
421                array( __CLASS__, 'restore_screen_id_after_wp_build' ),
422                function () {
423                    self::load_wp_build();
424                }
425            );
426            return;
427        }
428
429        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'alias_screen_id_for_wp_build' ) );
430        self::load_wp_build();
431        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'restore_screen_id_after_wp_build' ) );
432    }
433
434    /**
435     * Alias the current screen id to the wp-build page slug.
436     *
437     * The wp-build-generated enqueue callback only fires when the screen id
438     * equals the wp-build page slug. Our menu slug stays `jetpack-activity-log`,
439     * so alias the screen id in place to make the check pass without changing
440     * the user-facing URL. Hooked only for the Activity Log request, so this
441     * never affects any other screen.
442     *
443     * @since 0.4.1 Takes no argument; hooked on `admin_enqueue_scripts`.
444     *
445     * @return void
446     */
447    public static function alias_screen_id_for_wp_build() {
448        $screen = get_current_screen();
449        if ( ! $screen ) {
450            return;
451        }
452
453        self::$wp_build_original_screen_id = $screen->id;
454        $screen->id                        = self::WP_BUILD_PAGE_SLUG;
455    }
456
457    /**
458     * Undo alias_screen_id_for_wp_build(), so code after the generated check sees the real screen ID.
459     *
460     * @since 0.4.1
461     *
462     * @return void
463     */
464    public static function restore_screen_id_after_wp_build() {
465        $screen = get_current_screen();
466        if ( ! $screen || null === self::$wp_build_original_screen_id ) {
467            return;
468        }
469
470        $screen->id                        = self::$wp_build_original_screen_id;
471        self::$wp_build_original_screen_id = null;
472    }
473
474    /**
475     * Opt the dashboard's screen out of JITMs.
476     *
477     * @param string $screen_id The hook suffix the page was registered under, which is its screen ID.
478     * @return void
479     */
480    private static function opt_out_of_jitms( $screen_id ) {
481        self::$jitm_opt_out_screen_id = $screen_id;
482        add_filter( 'jetpack_display_jitms_on_screen', array( __CLASS__, 'hide_jitms_on_wp_build_dashboard' ), 10, 2 );
483    }
484
485    /**
486     * Keep JITMs off the wp-build dashboard, which has no `#jp-admin-notices` to show them in.
487     *
488     * Fetching a JITM records a view, so one the page hides would still be counted.
489     *
490     * @since 0.4.1
491     *
492     * @param bool   $show      Whether to show JITMs on the screen.
493     * @param string $screen_id The screen ID.
494     * @return bool
495     */
496    public static function hide_jitms_on_wp_build_dashboard( $show, $screen_id ) {
497        if ( null !== self::$jitm_opt_out_screen_id && self::$jitm_opt_out_screen_id === $screen_id ) {
498            return false;
499        }
500
501        return $show;
502    }
503
504    /**
505     * Print the React initial state and the Connection initial state, and load
506     * the Tracks transport.
507     *
508     * The initial state is attached to a dedicated empty classic handle because
509     * the dashboard is a wp-build script module — there is no classic bundle
510     * handle to hang the inline data on. Boot defers its own execution to
511     * `DOMContentLoaded`, so this inline data is always set on `window` first.
512     *
513     * `jp-tracks` (stats.wp.com/w.js) is required for analytics: the dashboard's
514     * `@automattic/jetpack-analytics` events only queue into `window._tkq`
515     * (the package's own w.js loader is disabled), so without this handle no
516     * `jetpack_activity_log_*` event ever flushes. Mirrors Newsletter's
517     * `Settings::load_admin_scripts()`.
518     *
519     * @return void
520     */
521    public static function enqueue_initial_state() {
522        wp_register_script( self::DATA_SCRIPT_HANDLE, false, array(), Package_Version::PACKAGE_VERSION, true );
523        wp_enqueue_script( self::DATA_SCRIPT_HANDLE );
524
525        // wp-build has no `jetpackConfig` external, so @automattic/jetpack-config (via AdminPage) reads this global.
526        wp_add_inline_script( self::DATA_SCRIPT_HANDLE, 'window.jetpackConfig = ' . wp_json_encode( array( 'consumer_slug' => self::PAGE_SLUG ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) . ';', 'before' );
527        wp_add_inline_script( self::DATA_SCRIPT_HANDLE, ( new Activity_Log_Initial_State() )->render(), 'before' );
528
529        // Simple has no Jetpack connection to describe; the app treats a missing state as no connection errors.
530        if ( ! self::is_wpcom_simple() ) {
531            Connection_Initial_State::render_script( self::DATA_SCRIPT_HANDLE );
532        }
533
534        wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
535
536        // The dashboard is a wp-build script module: it externalizes
537        // `@wordpress/i18n` to the shared `wp.i18n` global but has no
538        // `wp_set_script_translations()` equivalent to load its JS catalog.
539        // Enqueue Jetpack's i18n loader (`wp.jpI18nLoader`, from jetpack-assets,
540        // registered on `wp_default_scripts`) so the `@jetpack-activity-log/init`
541        // boot module can fetch and install the translation catalog before the
542        // app renders. Without this the UI ships in English on non-English sites.
543        if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
544            wp_enqueue_script( 'wp-jp-i18n-loader' );
545        }
546    }
547
548    /**
549     * Whether this is a WordPress.com Simple site.
550     *
551     * @since 0.6.0
552     *
553     * @return bool
554     */
555    public static function is_wpcom_simple() {
556        return ( new Host() )->is_wpcom_simple();
557    }
558
559    /**
560     * Fallback page body if the generated wp-build render function is
561     * unavailable (e.g. assets not built). Keeps the menu from fataling and
562     * still gives boot its mount container.
563     *
564     * @return void
565     */
566    public static function render_fallback() {
567        echo '<div class="wrap"><div id="jetpack-activity-log-dashboard-wp-admin-app"></div></div>';
568    }
569
570    /**
571     * Whether the current request targets the Activity Log admin page.
572     *
573     * The `$_GET['page']` value is populated by wp-admin/admin.php before any
574     * of our hooks fire, so this check is reliable from `admin_menu` onwards.
575     *
576     * @return bool
577     */
578    private static function is_activity_log_admin_request() {
579        if ( ! is_admin() || ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
580            return false;
581        }
582
583        return sanitize_text_field( wp_unslash( $_GET['page'] ) ) === self::PAGE_SLUG; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
584    }
585
586    /**
587     * Register the REST routes backing the Activity Log UI.
588     *
589     * Routes are added in Phase 2. This method exists now so that the
590     * `jetpack/v4/activity-log` namespace is reserved and the hook is wired.
591     */
592    public static function register_rest_routes() {
593        REST_Controller::register_rest_routes();
594    }
595}