| 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 |
|