PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.3
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 13.7.2 13.8.3 All 506 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-a.3, at jetpack_vendor/automattic/jetpack-activity-log/src/class-jetpack-activity-log.php

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