PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
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
← All changes | jetpack_vendor/automattic/jetpack-activity-log/src/class-jetpack-activity-log.php +504 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,504 @@
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 +}