PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-activity-log / src / class-jetpack-activity-log.php

class-jetpack-activity-log.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-activity-log/src/class-jetpack-activity-log.php

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