initial
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.8] - 2026-06-29
|
||||
### Changed
|
||||
- Update package dependencies. [#49271]
|
||||
|
||||
### Fixed
|
||||
- Admin: Align Text, Notice and Tabs.Panel props with the `@wordpress/ui` 0.15 API. [#49796]
|
||||
|
||||
## [0.1.7] - 2026-06-25
|
||||
### Changed
|
||||
- Update dependencies. [#49857]
|
||||
|
||||
## [0.1.6] - 2026-06-22
|
||||
### Changed
|
||||
- Update package dependencies. [#49631] [#49691] [#49757]
|
||||
|
||||
## [0.1.5] - 2026-06-15
|
||||
### Changed
|
||||
- Update package dependencies. [#49273]
|
||||
|
||||
## [0.1.4] - 2026-06-08
|
||||
### Changed
|
||||
- Update dependencies. [#49354]
|
||||
|
||||
## [0.1.3] - 2026-06-01
|
||||
### Changed
|
||||
- Update package dependencies. [#48404] [#49152]
|
||||
|
||||
## [0.1.2] - 2026-05-25
|
||||
### Changed
|
||||
- Normalize page tabs onto the shared minimal variant and `jp-admin-page-tabs--minimal` wrapper modifier. [#48964]
|
||||
- Update package dependencies. [#48405] [#49012]
|
||||
|
||||
## [0.1.1] - 2026-05-19
|
||||
### Changed
|
||||
- Build: Remove redundant dependency build script. [#48794]
|
||||
- Exclude development files from production builds. [#47365]
|
||||
- Update dependencies. [#48778]
|
||||
|
||||
## [0.1.0] - 2026-05-11
|
||||
### Added
|
||||
- Add initial package with page shell, REST namespace placeholder, and Active threats / History views. [#48458]
|
||||
- Analytics: Add Tracks events for DataViews interactions, Scan now, auto-fix, modal opens, successes, and failures. [#48458]
|
||||
- API: Add WPCOM bridge endpoints for scan reads, scan counts, scan enqueue, threat actions, and fix-status polling. [#48458]
|
||||
- DataViews: Persist search, filters, sort, pagination, and layout across reloads for Active threats and Scan history. [#48458]
|
||||
- Tests: Add PHPUnit route-registration coverage and Jest coverage for fix-status polling helpers. [#48458]
|
||||
- Threats: Add per-threat fix, ignore, unignore, and view-details modal flows. [#48458]
|
||||
- Threats: Add bulk auto-fix and Scan now flows with progress handling. [#48458]
|
||||
|
||||
### Changed
|
||||
- Admin: Adopt shared Jetpack AdminPage chrome, tab layout, footer handling, and full-height WP Admin page structure. [#48458]
|
||||
- Admin: Migrate the page from the webpack pipeline to a wp-build route. [#48458]
|
||||
- Admin: Silence standard WP Admin notices on the Scan page to avoid layout shifts during scans and fix flows. [#48458]
|
||||
- Admin: Update `@wordpress/admin-ui` to 2.0.0. [#48410]
|
||||
- DataViews: Use DataViews-managed empty states and remove duplicate in-table status filtering from the Scan panels. [#48458]
|
||||
- UI: Move tabs, empty states, stacks, buttons, dialog content, and notices toward `@wordpress/ui`. [#48458]
|
||||
|
||||
### Fixed
|
||||
- Admin: Fix full-height table and empty-state layout issues. [#48458]
|
||||
- API: Use blog-scoped WPCOM authentication for site-level scan reads and scan enqueue requests. [#48458]
|
||||
- Availability: Fix a connection-store crash by relying on server-side Scan availability checks. [#48458]
|
||||
- Build: Fix the wp-build production script and dependency build order so Scan builds from a fresh checkout. [#48458]
|
||||
- Styles: Use double quotes in the `[role="tabpanel"]` selector to satisfy style checks. [#48458]
|
||||
- Threats: Handle fix-status polling errors so fix modals no longer remain stuck in progress states. [#48458]
|
||||
|
||||
## 0.1.0-alpha - unreleased
|
||||
|
||||
Initial release.
|
||||
|
||||
[0.1.8]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.7...v0.1.8
|
||||
[0.1.7]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.6...v0.1.7
|
||||
[0.1.6]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.5...v0.1.6
|
||||
[0.1.5]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.4...v0.1.5
|
||||
[0.1.4]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.3...v0.1.4
|
||||
[0.1.3]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.2...v0.1.3
|
||||
[0.1.2]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.1...v0.1.2
|
||||
[0.1.1]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://github.com/Automattic/jetpack-scan-page/compare/v0.1.0-alpha...v0.1.0
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Main entry point for auto-generated asset registration.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
// Load script module registration.
|
||||
$modules_file = __DIR__ . '/modules.php';
|
||||
if ( file_exists( $modules_file ) ) {
|
||||
require_once $modules_file;
|
||||
}
|
||||
|
||||
// Load script registration.
|
||||
$scripts_file = __DIR__ . '/scripts.php';
|
||||
if ( file_exists( $scripts_file ) ) {
|
||||
require_once $scripts_file;
|
||||
}
|
||||
|
||||
// Load style registration.
|
||||
$styles_file = __DIR__ . '/styles.php';
|
||||
if ( file_exists( $styles_file ) ) {
|
||||
require_once $styles_file;
|
||||
}
|
||||
|
||||
// Load routes registration.
|
||||
$routes_file = __DIR__ . '/routes.php';
|
||||
if ( file_exists( $routes_file ) ) {
|
||||
require_once $routes_file;
|
||||
}
|
||||
|
||||
// Load widgets registration.
|
||||
$widgets_file = __DIR__ . '/widgets.php';
|
||||
if ( file_exists( $widgets_file ) ) {
|
||||
require_once $widgets_file;
|
||||
}
|
||||
|
||||
// Load pages registration.
|
||||
$pages_file = __DIR__ . '/pages.php';
|
||||
if ( file_exists( $pages_file ) ) {
|
||||
require_once $pages_file;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin constants - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* Returns an array of constants for use in other templates.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
return array(
|
||||
'version' => 'undefined',
|
||||
'build_url' => plugin_dir_url( __FILE__ ),
|
||||
);
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Script module registration - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register all script modules.
|
||||
*/
|
||||
function jetpack_scan_register_script_modules() {
|
||||
// Ensure this only runs once. wp_default_scripts can fire multiple times,
|
||||
// and each wp_deregister_script_module() call also dequeues the module.
|
||||
// If a module was enqueued between calls, repeated deregister/register
|
||||
// cycles would lose the enqueue state.
|
||||
static $already_registered = false;
|
||||
if ( $already_registered ) {
|
||||
return;
|
||||
}
|
||||
$already_registered = true;
|
||||
|
||||
// Load build constants
|
||||
$constants_file = __DIR__ . '/constants.php';
|
||||
if ( ! file_exists( $constants_file ) ) {
|
||||
return;
|
||||
}
|
||||
$build_constants = require $constants_file;
|
||||
$modules_dir = __DIR__ . '/modules';
|
||||
$modules_file = $modules_dir . '/registry.php';
|
||||
|
||||
if ( ! file_exists( $modules_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modules = require $modules_file;
|
||||
$base_url = $build_constants['build_url'] . 'modules/';
|
||||
|
||||
foreach ( $modules as $module ) {
|
||||
// WASM-only modules (e.g., vips worker) only have .min.js; use it even when SCRIPT_DEBUG is `true`.
|
||||
$extension = '.min.js';
|
||||
|
||||
$asset_path = $modules_dir . '/' . $module['asset'];
|
||||
$asset = file_exists( $asset_path ) ? require $asset_path : array();
|
||||
|
||||
// Deregister first to override any previously registered version
|
||||
// (e.g., Core's default modules when running as a plugin).
|
||||
wp_deregister_script_module( $module['id'] );
|
||||
|
||||
wp_register_script_module(
|
||||
$module['id'],
|
||||
$base_url . $module['path'] . $extension,
|
||||
$asset['module_dependencies'] ?? array(),
|
||||
$asset['version'] ?? false,
|
||||
array(
|
||||
'fetchpriority' => 'low',
|
||||
'in_footer' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'wp_default_scripts', 'jetpack_scan_register_script_modules' );
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-commands', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-notices', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/lazy-editor', 'import' => 'dynamic'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => '188faf6c64fdd1b4');
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Script module registry - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
return array(
|
||||
|
||||
);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Pages loader - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
foreach ( [
|
||||
__DIR__ . '/pages/jetpack-scan/page.php',
|
||||
__DIR__ . '/pages/jetpack-scan/page-wp-admin.php',
|
||||
] as $file ) {
|
||||
if ( file_exists( $file ) ) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
// Empty module loader for page dependencies
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
/**
|
||||
* Page: jetpack-scan (wp-admin integrated)
|
||||
* Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* This version integrates with the standard WordPress admin interface,
|
||||
* keeping the wp-admin sidebar and scripts/styles intact.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
// Global storage for jetpack-scan routes and menu items
|
||||
global $jetpack_scan_jetpack_scan_wp_admin_routes, $jetpack_scan_jetpack_scan_wp_admin_menu_items;
|
||||
$jetpack_scan_jetpack_scan_wp_admin_routes = array();
|
||||
$jetpack_scan_jetpack_scan_wp_admin_menu_items = array();
|
||||
|
||||
/**
|
||||
* Register a route for the jetpack-scan-wp-admin page.
|
||||
*
|
||||
* @param string $path Route path (e.g., '/types/$type/edit/$id').
|
||||
* @param string|null $content_module Script module ID for content (stage/inspector).
|
||||
* @param string|null $route_module Script module ID for route lifecycle hooks.
|
||||
*/
|
||||
function jetpack_scan_register_jetpack_scan_wp_admin_route( $path, $content_module = null, $route_module = null ) {
|
||||
global $jetpack_scan_jetpack_scan_wp_admin_routes;
|
||||
|
||||
$route = array( 'path' => $path );
|
||||
if ( ! empty( $content_module ) ) {
|
||||
$route['content_module'] = $content_module;
|
||||
}
|
||||
if ( ! empty( $route_module ) ) {
|
||||
$route['route_module'] = $route_module;
|
||||
}
|
||||
|
||||
$jetpack_scan_jetpack_scan_wp_admin_routes[] = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a menu item for the jetpack-scan-wp-admin page.
|
||||
* Note: Menu items are registered but not displayed in single-page mode.
|
||||
*
|
||||
* @param string $id Menu item ID.
|
||||
* @param string $label Display label.
|
||||
* @param string $to Route path to navigate to.
|
||||
* @param string $parent_id Optional. Parent menu item ID.
|
||||
*/
|
||||
function jetpack_scan_register_jetpack_scan_wp_admin_menu_item( $id, $label, $to, $parent_id = '' ) {
|
||||
global $jetpack_scan_jetpack_scan_wp_admin_menu_items;
|
||||
|
||||
$menu_item = array(
|
||||
'id' => $id,
|
||||
'label' => $label,
|
||||
'to' => $to,
|
||||
);
|
||||
|
||||
if ( ! empty( $parent_id ) ) {
|
||||
$menu_item['parent'] = $parent_id;
|
||||
}
|
||||
|
||||
$jetpack_scan_jetpack_scan_wp_admin_menu_items[] = $menu_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered routes for the jetpack-scan-wp-admin page.
|
||||
*
|
||||
* @return array Array of route objects.
|
||||
*/
|
||||
function jetpack_scan_get_jetpack_scan_wp_admin_routes() {
|
||||
global $jetpack_scan_jetpack_scan_wp_admin_routes;
|
||||
return $jetpack_scan_jetpack_scan_wp_admin_routes ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered menu items for the jetpack-scan-wp-admin page.
|
||||
*
|
||||
* @return array Array of menu item objects.
|
||||
*/
|
||||
function jetpack_scan_get_jetpack_scan_wp_admin_menu_items() {
|
||||
global $jetpack_scan_jetpack_scan_wp_admin_menu_items;
|
||||
return $jetpack_scan_jetpack_scan_wp_admin_menu_items ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload REST API data for the jetpack-scan-wp-admin page.
|
||||
* Automatically called during page rendering.
|
||||
*/
|
||||
function jetpack_scan_jetpack_scan_wp_admin_preload_data() {
|
||||
// Define paths to preload - same for all pages
|
||||
// Please also change packages/core-data/src/entities.js when changing this.
|
||||
$preload_paths = array(
|
||||
'/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
|
||||
array( '/wp/v2/settings', 'OPTIONS' ),
|
||||
);
|
||||
|
||||
// Use rest_preload_api_request to gather the preloaded data
|
||||
$preload_data = array_reduce(
|
||||
$preload_paths,
|
||||
'rest_preload_api_request',
|
||||
array()
|
||||
);
|
||||
|
||||
// Register the preloading middleware with wp-api-fetch
|
||||
wp_add_inline_script(
|
||||
'wp-api-fetch',
|
||||
sprintf(
|
||||
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
|
||||
wp_json_encode( $preload_data )
|
||||
),
|
||||
'after'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts and styles for the jetpack-scan-wp-admin page.
|
||||
* Hooked to admin_enqueue_scripts.
|
||||
*
|
||||
* @param string $hook_suffix The current admin page.
|
||||
*/
|
||||
function jetpack_scan_jetpack_scan_wp_admin_enqueue_scripts( $hook_suffix ) {
|
||||
// Check all possible ways this page can be accessed:
|
||||
// 1. Menu page via admin.php?page=jetpack-scan-wp-admin (plugin)
|
||||
// 2. Direct file via jetpack-scan.php (Core) - screen ID will be 'jetpack-scan'
|
||||
$current_screen = get_current_screen();
|
||||
$is_our_page = (
|
||||
( isset( $_GET['page'] ) && 'jetpack-scan-wp-admin' === $_GET['page'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
( $current_screen && 'jetpack-scan' === $current_screen->id )
|
||||
);
|
||||
|
||||
if ( ! $is_our_page ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load build constants
|
||||
$build_constants = require __DIR__ . '/../../constants.php';
|
||||
|
||||
/**
|
||||
* Fires when the jetpack-scan admin page is initialized so extensions can register routes and menu items.
|
||||
*/
|
||||
do_action( 'jetpack-scan-wp-admin_init' );
|
||||
|
||||
// Preload REST API data
|
||||
jetpack_scan_jetpack_scan_wp_admin_preload_data();
|
||||
|
||||
// Get all registered routes
|
||||
$routes = jetpack_scan_get_jetpack_scan_wp_admin_routes();
|
||||
|
||||
// Get boot module asset file for dependencies
|
||||
$asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
|
||||
if ( file_exists( $asset_file ) ) {
|
||||
$asset = require $asset_file;
|
||||
|
||||
// This script serves two purposes:
|
||||
// 1. It ensures all the globals that are made available to the modules are loaded.
|
||||
// 2. It initializes the boot module as an inline script.
|
||||
wp_register_script( 'jetpack-scan-wp-admin-prerequisites', '', $asset['dependencies'], $asset['version'], true );
|
||||
|
||||
/*
|
||||
* Add inline script to initialize the app using initSinglePage (no menuItems).
|
||||
* The dynamic import is deferred until DOMContentLoaded so that all classic
|
||||
* script dependencies of @wordpress/boot (wp-private-apis, wp-components,
|
||||
* wp-theme, etc.) have finished parsing and executing before the boot module
|
||||
* evaluates. Otherwise, a modulepreloaded @wordpress/boot can win the race
|
||||
* against the classic-script-printing pass on fast CDN-fronted hosts in
|
||||
* Chrome, evaluating before wp.theme.privateApis is defined and throwing
|
||||
* "Cannot unlock an undefined object". See <https://core.trac.wordpress.org/ticket/65103>.
|
||||
*/
|
||||
$init_js_function = <<<'JS'
|
||||
( mountId, routes ) => {
|
||||
const run = async () => {
|
||||
const mod = await import( "@wordpress/boot" );
|
||||
mod.initSinglePage( { mountId, routes } );
|
||||
};
|
||||
if ( document.readyState === "loading" ) {
|
||||
document.addEventListener( "DOMContentLoaded", run );
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
JS;
|
||||
wp_add_inline_script(
|
||||
'jetpack-scan-wp-admin-prerequisites',
|
||||
sprintf(
|
||||
'( %s )( %s, %s );',
|
||||
$init_js_function,
|
||||
wp_json_encode( 'jetpack-scan-wp-admin-app', JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
|
||||
wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
|
||||
)
|
||||
);
|
||||
|
||||
// Register prerequisites style by filtering script dependencies to find registered styles
|
||||
$style_dependencies = array_filter(
|
||||
$asset['dependencies'],
|
||||
function ( $handle ) {
|
||||
return wp_style_is( $handle, 'registered' );
|
||||
}
|
||||
);
|
||||
wp_register_style( 'jetpack-scan-wp-admin-prerequisites', false, $style_dependencies, $asset['version'] );
|
||||
|
||||
// Build dependencies for jetpack-scan-wp-admin module
|
||||
$boot_dependencies = array(
|
||||
array(
|
||||
'import' => 'static',
|
||||
'id' => '@wordpress/boot',
|
||||
),
|
||||
);
|
||||
|
||||
// Add all registered routes as dependencies
|
||||
foreach ( $routes as $route ) {
|
||||
if ( isset( $route['route_module'] ) ) {
|
||||
$boot_dependencies[] = array(
|
||||
'import' => 'static',
|
||||
'id' => $route['route_module'],
|
||||
);
|
||||
}
|
||||
if ( isset( $route['content_module'] ) ) {
|
||||
$boot_dependencies[] = array(
|
||||
'import' => 'dynamic',
|
||||
'id' => $route['content_module'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the boot script-module dependencies for the
|
||||
* jetpack-scan-wp-admin page.
|
||||
*
|
||||
* Surfaces extending this page can append entries to the boot
|
||||
* dependency list. Each entry is an array with 'import' (string
|
||||
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
|
||||
*
|
||||
* @param array $boot_dependencies Boot dependencies for the page.
|
||||
*/
|
||||
$boot_dependencies = apply_filters(
|
||||
'jetpack-scan-wp-admin_boot_dependencies',
|
||||
$boot_dependencies
|
||||
);
|
||||
|
||||
// Dummy script module to ensure dependencies are loaded
|
||||
wp_register_script_module(
|
||||
'jetpack-scan-wp-admin',
|
||||
$build_constants['build_url'] . 'pages/jetpack-scan/loader.js',
|
||||
$boot_dependencies
|
||||
);
|
||||
|
||||
// Enqueue the boot scripts and styles
|
||||
wp_enqueue_script( 'jetpack-scan-wp-admin-prerequisites' );
|
||||
wp_enqueue_script_module( 'jetpack-scan-wp-admin' );
|
||||
wp_enqueue_style( 'jetpack-scan-wp-admin-prerequisites' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the jetpack-scan-wp-admin page.
|
||||
* Call this function from add_menu_page or add_submenu_page.
|
||||
* This renders within the normal WordPress admin interface.
|
||||
*/
|
||||
function jetpack_scan_jetpack_scan_wp_admin_render_page() {
|
||||
?>
|
||||
<style>
|
||||
/* Critical styles to prevent layout shifts - inlined for immediate application */
|
||||
|
||||
#wpwrap {
|
||||
overflow-y: auto;
|
||||
}
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Reset wp-admin padding */
|
||||
#wpcontent {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
#wpbody-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Hide legacy admin elements */
|
||||
#wpbody-content > div:not(.boot-layout-container):not(#screen-meta) {
|
||||
display: none;
|
||||
}
|
||||
#wpfooter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Accessibility regions */
|
||||
.a11y-speak-region {
|
||||
inset-inline-start: -1px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
/* Admin menu indicators */
|
||||
ul#adminmenu a.wp-has-current-submenu::after,
|
||||
ul#adminmenu > li.current > a.current::after {
|
||||
border-inline-end-color: #fff;
|
||||
}
|
||||
|
||||
/* Media frame fix */
|
||||
.media-frame select.attachment-filters:last-of-type {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive overflow fix for #wpwrap */
|
||||
@media (min-width: 782px) {
|
||||
#wpwrap {
|
||||
overflow-y: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div id="jetpack-scan-wp-admin-app" class="boot-layout-container"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// Hook the enqueue function to admin_enqueue_scripts
|
||||
add_action( 'admin_enqueue_scripts', 'jetpack_scan_jetpack_scan_wp_admin_enqueue_scripts' );
|
||||
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
/**
|
||||
* Page: jetpack-scan
|
||||
* Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
// Global storage for jetpack-scan routes and menu items
|
||||
global $jetpack_scan_jetpack_scan_routes, $jetpack_scan_jetpack_scan_menu_items;
|
||||
$jetpack_scan_jetpack_scan_routes = array();
|
||||
$jetpack_scan_jetpack_scan_menu_items = array();
|
||||
|
||||
/**
|
||||
* Register a route for the jetpack-scan page.
|
||||
*
|
||||
* @param string $path Route path (e.g., '/types/$type/edit/$id').
|
||||
* @param string|null $content_module Script module ID for content (stage/inspector).
|
||||
* @param string|null $route_module Script module ID for route lifecycle hooks.
|
||||
*/
|
||||
function jetpack_scan_register_jetpack_scan_route( $path, $content_module = null, $route_module = null ) {
|
||||
global $jetpack_scan_jetpack_scan_routes;
|
||||
|
||||
$route = array( 'path' => $path );
|
||||
if ( ! empty( $content_module ) ) {
|
||||
$route['content_module'] = $content_module;
|
||||
}
|
||||
if ( ! empty( $route_module ) ) {
|
||||
$route['route_module'] = $route_module;
|
||||
}
|
||||
|
||||
$jetpack_scan_jetpack_scan_routes[] = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a menu item for the jetpack-scan page.
|
||||
*
|
||||
* @param string $id Menu item ID.
|
||||
* @param string $label Display label.
|
||||
* @param string $to Route path to navigate to.
|
||||
* @param string $parent_id Optional. Parent menu item ID.
|
||||
* @param string $parent_type Optional. Parent type: 'drilldown' or 'dropdown'.
|
||||
*/
|
||||
function jetpack_scan_register_jetpack_scan_menu_item( $id, $label, $to, $parent_id = '', $parent_type = '' ) {
|
||||
global $jetpack_scan_jetpack_scan_menu_items;
|
||||
|
||||
$menu_item = array(
|
||||
'id' => $id,
|
||||
'label' => $label,
|
||||
'to' => $to,
|
||||
);
|
||||
|
||||
if ( ! empty( $parent_id ) ) {
|
||||
$menu_item['parent'] = $parent_id;
|
||||
}
|
||||
|
||||
if ( ! empty( $parent_type ) && in_array( $parent_type, array( 'drilldown', 'dropdown' ), true ) ) {
|
||||
$menu_item['parent_type'] = $parent_type;
|
||||
}
|
||||
|
||||
$jetpack_scan_jetpack_scan_menu_items[] = $menu_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered routes for the jetpack-scan page.
|
||||
*
|
||||
* @return array Array of route objects.
|
||||
*/
|
||||
function jetpack_scan_get_jetpack_scan_routes() {
|
||||
global $jetpack_scan_jetpack_scan_routes;
|
||||
return $jetpack_scan_jetpack_scan_routes ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered menu items for the jetpack-scan page.
|
||||
*
|
||||
* @return array Array of menu item objects.
|
||||
*/
|
||||
function jetpack_scan_get_jetpack_scan_menu_items() {
|
||||
global $jetpack_scan_jetpack_scan_menu_items;
|
||||
return $jetpack_scan_jetpack_scan_menu_items ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload REST API data for the jetpack-scan page.
|
||||
* Automatically called during page rendering.
|
||||
*/
|
||||
function jetpack_scan_jetpack_scan_preload_data() {
|
||||
// Define paths to preload - same for all pages
|
||||
// Please also change packages/core-data/src/entities.js when changing this.
|
||||
$preload_paths = array(
|
||||
'/?_fields=description,gmt_offset,home,image_sizes,image_size_threshold,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front',
|
||||
array( '/wp/v2/settings', 'OPTIONS' ),
|
||||
);
|
||||
|
||||
// Use rest_preload_api_request to gather the preloaded data
|
||||
$preload_data = array_reduce(
|
||||
$preload_paths,
|
||||
'rest_preload_api_request',
|
||||
array()
|
||||
);
|
||||
|
||||
// Register the preloading middleware with wp-api-fetch
|
||||
wp_add_inline_script(
|
||||
'wp-api-fetch',
|
||||
sprintf(
|
||||
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
|
||||
wp_json_encode( $preload_data )
|
||||
),
|
||||
'after'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the jetpack-scan page.
|
||||
* Call this function from add_menu_page or add_submenu_page.
|
||||
*/
|
||||
function jetpack_scan_jetpack_scan_render_page() {
|
||||
// Load build constants
|
||||
$build_constants = require __DIR__ . '/../../constants.php';
|
||||
|
||||
// Set current screen
|
||||
set_current_screen();
|
||||
|
||||
// Remove unwanted deprecated handler
|
||||
remove_action( 'admin_head', 'wp_admin_bar_header' );
|
||||
|
||||
// Remove unwanted scripts and styles that were enqueued during `admin_init`
|
||||
foreach ( wp_scripts()->queue as $script ) {
|
||||
wp_dequeue_script( $script );
|
||||
}
|
||||
foreach ( wp_styles()->queue as $style ) {
|
||||
wp_dequeue_style( $style );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the jetpack-scan page is initialized so extensions can register routes and menu items.
|
||||
*/
|
||||
do_action( 'jetpack-scan_init' );
|
||||
|
||||
// Enqueue command palette assets for boot-based pages
|
||||
if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
|
||||
wp_enqueue_command_palette_assets();
|
||||
}
|
||||
|
||||
// Preload REST API data
|
||||
jetpack_scan_jetpack_scan_preload_data();
|
||||
|
||||
// Get all registered routes and menu items
|
||||
$menu_items = jetpack_scan_get_jetpack_scan_menu_items();
|
||||
$routes = jetpack_scan_get_jetpack_scan_routes();
|
||||
|
||||
// Get boot module asset file for dependencies
|
||||
$asset_file = __DIR__ . '/../../modules/boot/index.min.asset.php';
|
||||
if ( file_exists( $asset_file ) ) {
|
||||
$asset = require $asset_file;
|
||||
|
||||
// This script serves two purposes:
|
||||
// 1. It ensures all the globals that are made available to the modules are loaded.
|
||||
// 2. It initializes the boot module as an inline script.
|
||||
wp_register_script( 'jetpack-scan-prerequisites', '', $asset['dependencies'], $asset['version'], true );
|
||||
|
||||
// Add inline script to initialize the app
|
||||
$init_modules = [];
|
||||
wp_add_inline_script(
|
||||
'jetpack-scan-prerequisites',
|
||||
sprintf(
|
||||
'import("@wordpress/boot").then(mod => mod.init({mountId: "%s", menuItems: %s, routes: %s, initModules: %s, dashboardLink: "%s"}));',
|
||||
'jetpack-scan-app',
|
||||
wp_json_encode( $menu_items, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
|
||||
wp_json_encode( $routes, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
|
||||
wp_json_encode( $init_modules, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
|
||||
esc_url( admin_url( '/' ) )
|
||||
)
|
||||
);
|
||||
|
||||
// Register prerequisites style by filtering script dependencies to find registered styles
|
||||
$style_dependencies = array_filter(
|
||||
$asset['dependencies'],
|
||||
function ( $handle ) {
|
||||
return wp_style_is( $handle, 'registered' );
|
||||
}
|
||||
);
|
||||
wp_register_style( 'jetpack-scan-prerequisites', false, $style_dependencies, $asset['version'] );
|
||||
|
||||
// Build dependencies for jetpack-scan module
|
||||
$boot_dependencies = array(
|
||||
array(
|
||||
'import' => 'static',
|
||||
'id' => '@wordpress/boot',
|
||||
),
|
||||
);
|
||||
|
||||
// Add init modules as static dependencies
|
||||
// No init modules configured
|
||||
|
||||
// Add all registered routes as dependencies
|
||||
foreach ( $routes as $route ) {
|
||||
if ( isset( $route['route_module'] ) ) {
|
||||
$boot_dependencies[] = array(
|
||||
'import' => 'static',
|
||||
'id' => $route['route_module'],
|
||||
);
|
||||
}
|
||||
if ( isset( $route['content_module'] ) ) {
|
||||
$boot_dependencies[] = array(
|
||||
'import' => 'dynamic',
|
||||
'id' => $route['content_module'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the boot script-module dependencies for the
|
||||
* jetpack-scan page.
|
||||
*
|
||||
* Surfaces extending this page can append entries to the boot
|
||||
* dependency list. Each entry is an array with 'import' (string
|
||||
* 'static' or 'dynamic') and 'id' (script-module handle) keys.
|
||||
*
|
||||
* @param array $boot_dependencies Boot dependencies for the page.
|
||||
*/
|
||||
$boot_dependencies = apply_filters(
|
||||
'jetpack-scan_boot_dependencies',
|
||||
$boot_dependencies
|
||||
);
|
||||
|
||||
// Dummy script module to ensure dependencies are loaded
|
||||
wp_register_script_module(
|
||||
'jetpack-scan',
|
||||
$build_constants['build_url'] . 'pages/jetpack-scan/loader.js',
|
||||
$boot_dependencies
|
||||
);
|
||||
|
||||
// Enqueue the boot scripts and styles
|
||||
wp_enqueue_script( 'jetpack-scan-prerequisites' );
|
||||
wp_enqueue_script_module( 'jetpack-scan' );
|
||||
wp_enqueue_style( 'jetpack-scan-prerequisites' );
|
||||
}
|
||||
|
||||
// Output the HTML
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html <?php language_attributes(); ?>>
|
||||
<head>
|
||||
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?php echo esc_html( get_admin_page_title() ); ?></title>
|
||||
<style>
|
||||
html {
|
||||
background: #f1f1f1;
|
||||
color: #444;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
#wpadminbar { display: none; }
|
||||
</style>
|
||||
<?php
|
||||
global $hook_suffix;
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$hook_suffix = 'jetpack-scan';
|
||||
|
||||
// BEGIN see wp-admin/admin-header.php
|
||||
print_admin_styles();
|
||||
print_head_scripts();
|
||||
|
||||
/** This action is documented in wp-admin/admin-header.php */
|
||||
do_action( "admin_head-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
|
||||
|
||||
/** This action is documented in wp-admin/admin-header.php */
|
||||
do_action( 'admin_head' );
|
||||
// END see wp-admin/admin-header.php
|
||||
?>
|
||||
</head>
|
||||
<body class="jetpack-scan">
|
||||
<div id="jetpack-scan-app" style="height: 100vh; box-sizing: border-box;"></div>
|
||||
<?php
|
||||
// BEGIN see wp-admin/admin-footer.php
|
||||
|
||||
/** This action is documented in wp-admin/admin-footer.php */
|
||||
do_action( 'admin_footer', '' );
|
||||
|
||||
// Print import map first so it's available for inline scripts
|
||||
wp_script_modules()->print_import_map();
|
||||
print_footer_scripts();
|
||||
wp_script_modules()->print_enqueued_script_modules();
|
||||
wp_script_modules()->print_script_module_preloads();
|
||||
wp_script_modules()->print_script_module_data();
|
||||
|
||||
/** This action is documented in wp-admin/admin-footer.php */
|
||||
do_action( "admin_footer-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
|
||||
// END see wp-admin/admin-footer.php
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept admin_init to render the page early.
|
||||
* This bypasses the default WordPress admin template.
|
||||
*/
|
||||
function jetpack_scan_jetpack_scan_intercept_render() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( isset( $_GET['page'] ) && 'jetpack-scan' === $_GET['page'] ) {
|
||||
jetpack_scan_jetpack_scan_render_page();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Hook the interceptor to admin_init
|
||||
add_action( 'admin_init', 'jetpack_scan_jetpack_scan_intercept_render' );
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Routes registration - Auto-generated by build process.
|
||||
* Registers all routes on their respective page init hooks.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
// Load routes registry
|
||||
$routes_file = __DIR__ . '/routes/registry.php';
|
||||
if ( ! file_exists( $routes_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$routes = require $routes_file;
|
||||
|
||||
// Group routes by page and store in globals for page-specific functions
|
||||
$routes_by_page = array();
|
||||
foreach ( $routes as $route ) {
|
||||
$page_slug = $route['page'];
|
||||
if ( ! isset( $routes_by_page[ $page_slug ] ) ) {
|
||||
$routes_by_page[ $page_slug ] = array();
|
||||
}
|
||||
$routes_by_page[ $page_slug ][] = $route;
|
||||
}
|
||||
|
||||
// Store routes data in globals for each page
|
||||
foreach ( $routes_by_page as $page_slug => $page_routes ) {
|
||||
$page_slug_underscore = str_replace( '-', '_', $page_slug );
|
||||
$global_name = 'jetpack_scan_' . $page_slug_underscore . '_routes_data';
|
||||
$GLOBALS[ $global_name ] = $page_routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic helper function to register routes for a page.
|
||||
*
|
||||
* @param array $page_routes Array of route data for the page.
|
||||
* @param string $register_function_name Name of the function to call for registering each route.
|
||||
*/
|
||||
function jetpack_scan_register_page_routes( $page_routes, $register_function_name ) {
|
||||
// Load build constants
|
||||
$build_constants = require __DIR__ . '/constants.php';
|
||||
|
||||
foreach ( $page_routes as $route ) {
|
||||
$content_handle = null;
|
||||
$route_handle = null;
|
||||
|
||||
// Register content module if exists
|
||||
if ( $route['has_content'] ) {
|
||||
$content_asset_path = __DIR__ . "/routes/{$route['name']}/content.min.asset.php";
|
||||
if ( file_exists( $content_asset_path ) ) {
|
||||
$content_asset = require $content_asset_path;
|
||||
$content_handle = 'jetpack-scan/routes/' . $route['name'] . '/content';
|
||||
$extension = '.min.js';
|
||||
// Deregister first to override any previously registered version
|
||||
// (e.g., Core's default modules when running as a plugin).
|
||||
wp_deregister_script_module( $content_handle );
|
||||
wp_register_script_module(
|
||||
$content_handle,
|
||||
$build_constants['build_url'] . 'routes/' . $route['name'] . '/content' . $extension,
|
||||
$content_asset['module_dependencies'] ?? array(),
|
||||
$content_asset['version'] ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Register route module if exists
|
||||
if ( $route['has_route'] ) {
|
||||
$route_asset_path = __DIR__ . "/routes/{$route['name']}/route.min.asset.php";
|
||||
if ( file_exists( $route_asset_path ) ) {
|
||||
$route_asset = require $route_asset_path;
|
||||
$route_handle = 'jetpack-scan/routes/' . $route['name'] . '/route';
|
||||
$extension = '.min.js';
|
||||
// Deregister first to override any previously registered version
|
||||
// (e.g., Core's default modules when running as a plugin).
|
||||
wp_deregister_script_module( $route_handle );
|
||||
wp_register_script_module(
|
||||
$route_handle,
|
||||
$build_constants['build_url'] . 'routes/' . $route['name'] . '/route' . $extension,
|
||||
$route_asset['module_dependencies'] ?? array(),
|
||||
$route_asset['version'] ?? false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Register route with page
|
||||
if ( function_exists( $register_function_name ) ) {
|
||||
call_user_func( $register_function_name, $route['path'], $content_handle, $route_handle );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page-specific route registration functions
|
||||
// Page-specific route registration functions for jetpack-scan
|
||||
/**
|
||||
* Register routes for jetpack-scan page (full-page mode).
|
||||
*/
|
||||
function jetpack_scan_register_jetpack_scan_page_routes() {
|
||||
global $jetpack_scan_jetpack_scan_routes_data;
|
||||
jetpack_scan_register_page_routes( $jetpack_scan_jetpack_scan_routes_data, 'jetpack_scan_register_jetpack_scan_route' );
|
||||
}
|
||||
add_action( 'jetpack-scan_init', 'jetpack_scan_register_jetpack_scan_page_routes' );
|
||||
|
||||
/**
|
||||
* Register routes for jetpack-scan page (wp-admin mode).
|
||||
*/
|
||||
function jetpack_scan_register_jetpack_scan_wp_admin_page_routes() {
|
||||
global $jetpack_scan_jetpack_scan_routes_data;
|
||||
jetpack_scan_register_page_routes( $jetpack_scan_jetpack_scan_routes_data, 'jetpack_scan_register_jetpack_scan_wp_admin_route' );
|
||||
}
|
||||
add_action( 'jetpack-scan-wp-admin_init', 'jetpack_scan_register_jetpack_scan_wp_admin_page_routes' );
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-i18n', 'wp-keycodes', 'wp-notices', 'wp-primitives', 'wp-private-apis', 'wp-theme', 'wp-url', 'wp-warning'), 'module_dependencies' => array(array('id' => '@wordpress/a11y', 'import' => 'static'), array('id' => '@wordpress/route', 'import' => 'static')), 'version' => '63b5fff8850fad3669d1');
|
||||
+38
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'version' => '051fa939bb02df0ad906');
|
||||
+1
@@ -0,0 +1 @@
|
||||
var e={inspector:()=>!1};export{e as route};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Route registry - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
return array(
|
||||
array(
|
||||
'name' => 'index',
|
||||
'path' => '/',
|
||||
'page' => 'jetpack-scan',
|
||||
'has_route' => true,
|
||||
'has_content' => true,
|
||||
)
|
||||
);
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* Script registration - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registers a script according to `wp_register_script`. Honors this request by
|
||||
* reassigning internal dependency properties of any script handle already
|
||||
* registered by that name. It does not deregister the original script, to
|
||||
* avoid losing inline scripts which may have been attached.
|
||||
*
|
||||
* @param WP_Scripts $scripts WP_Scripts instance.
|
||||
* @param string $handle Name of the script. Should be unique.
|
||||
* @param string $src Full URL of the script, or path of the script relative to the WordPress root directory.
|
||||
* @param array $deps Optional. An array of registered script handles this script depends on. Default empty array.
|
||||
* @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL
|
||||
* as a query string for cache busting purposes. If version is set to false, a version
|
||||
* number is automatically added equal to current installed WordPress version.
|
||||
* If set to null, no version is added.
|
||||
* @param bool $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>.
|
||||
* Default 'false'.
|
||||
*/
|
||||
function jetpack_scan_override_script( $scripts, $handle, $src, $deps = array(), $ver = false, $in_footer = false ) {
|
||||
$script = $scripts->query( $handle, 'registered' );
|
||||
if ( $script ) {
|
||||
/*
|
||||
* In many ways, this is a reimplementation of `wp_register_script` but
|
||||
* bypassing consideration of whether a script by the given handle had
|
||||
* already been registered.
|
||||
*/
|
||||
|
||||
// See: `_WP_Dependency::__construct` .
|
||||
$script->src = $src;
|
||||
$script->deps = $deps;
|
||||
$script->ver = $ver;
|
||||
$script->args = $in_footer ? 1 : null;
|
||||
} else {
|
||||
$scripts->add( $handle, $src, $deps, $ver, ( $in_footer ? 1 : null ) );
|
||||
}
|
||||
|
||||
if ( in_array( 'wp-i18n', $deps, true ) ) {
|
||||
$scripts->set_translations( $handle );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all package scripts.
|
||||
*/
|
||||
function jetpack_scan_register_package_scripts( $scripts ) {
|
||||
// Load build constants
|
||||
$build_constants = require __DIR__ . '/constants.php';
|
||||
$default_version = ! SCRIPT_DEBUG ? $build_constants['version'] : time();
|
||||
$extension = '.min.js';
|
||||
|
||||
$scripts_dir = __DIR__ . '/scripts';
|
||||
$scripts_file = $scripts_dir . '/registry.php';
|
||||
|
||||
if ( ! file_exists( $scripts_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scripts_data = require $scripts_file;
|
||||
$plugin_dir = dirname( __FILE__ );
|
||||
|
||||
foreach ( $scripts_data as $script_data ) {
|
||||
$asset_file = $scripts_dir . '/' . $script_data['asset'];
|
||||
$asset = file_exists( $asset_file ) ? require $asset_file : array();
|
||||
$dependencies = $asset['dependencies'] ?? array();
|
||||
$version = $asset['version'] ?? $default_version;
|
||||
|
||||
jetpack_scan_override_script(
|
||||
$scripts,
|
||||
$script_data['handle'],
|
||||
$build_constants['build_url'] . 'scripts/' . $script_data['path'] . $extension,
|
||||
$dependencies,
|
||||
$version,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'wp_default_scripts', 'jetpack_scan_register_package_scripts' );
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Script registry - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
return array(
|
||||
|
||||
);
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* Style registration - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* Note: These styles can be overridden by calling jetpack_scan_override_style()
|
||||
* in lib/client-assets.php for complex cases (multiple files, conditional deps, etc.)
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registers a style according to `wp_register_style`. Honors this request by
|
||||
* deregistering any style by the same handler before registration.
|
||||
*
|
||||
* @param WP_Styles $styles WP_Styles instance.
|
||||
* @param string $handle Name of the stylesheet. Should be unique.
|
||||
* @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory.
|
||||
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
|
||||
* @param string|bool|null $ver Optional. String specifying stylesheet version number, if it has one, which is added to the URL
|
||||
* as a query string for cache busting purposes. If version is set to false, a version
|
||||
* number is automatically added equal to current installed WordPress version.
|
||||
* If set to null, no version is added.
|
||||
* @param string $media Optional. The media for which this stylesheet has been defined.
|
||||
* Default 'all'. Accepts media types like 'all', 'print' and 'screen', or media queries like
|
||||
* '(orientation: portrait)' and '(max-width: 640px)'.
|
||||
*/
|
||||
function jetpack_scan_override_style( $styles, $handle, $src, $deps = array(), $ver = false, $media = 'all' ) {
|
||||
$style = $styles->query( $handle, 'registered' );
|
||||
if ( $style ) {
|
||||
$styles->remove( $handle );
|
||||
}
|
||||
$styles->add( $handle, $src, $deps, $ver, $media );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all package styles (simple cases only).
|
||||
* Complex cases should be manually registered in lib/client-assets.php.
|
||||
*
|
||||
* @param WP_Styles $styles WP_Styles instance.
|
||||
*/
|
||||
function jetpack_scan_register_package_styles( $styles ) {
|
||||
// Load build constants
|
||||
$build_constants = require __DIR__ . '/constants.php';
|
||||
$default_version = ! SCRIPT_DEBUG ? $build_constants['version'] : time();
|
||||
$suffix = '.min';
|
||||
|
||||
$styles_dir = __DIR__ . '/styles';
|
||||
$styles_file = $styles_dir . '/registry.php';
|
||||
|
||||
if ( ! file_exists( $styles_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$styles_data = require $styles_file;
|
||||
$plugin_dir = dirname( __FILE__ );
|
||||
|
||||
foreach ( $styles_data as $style_data ) {
|
||||
jetpack_scan_override_style(
|
||||
$styles,
|
||||
$style_data['handle'],
|
||||
$build_constants['build_url'] . 'styles/' . $style_data['path'] . $suffix . '.css',
|
||||
$style_data['dependencies'],
|
||||
$default_version
|
||||
);
|
||||
|
||||
// Enable RTL support (WordPress automatically loads -rtl.css variant)
|
||||
$styles->add_data( $style_data['handle'], 'rtl', 'replace' );
|
||||
$styles->add_data( $style_data['handle'], 'suffix', $suffix );
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'wp_default_styles', 'jetpack_scan_register_package_styles' );
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Style registry - Auto-generated by build process.
|
||||
* Do not edit this file manually.
|
||||
*
|
||||
* @package jetpack_scan
|
||||
*/
|
||||
|
||||
return array(
|
||||
|
||||
);
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* The Scan React initial state.
|
||||
*
|
||||
* @package automattic/jetpack-scan-page
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Scan_Page;
|
||||
|
||||
use Automattic\Jetpack\Status;
|
||||
use Jetpack_Options;
|
||||
use function admin_url;
|
||||
use function esc_url_raw;
|
||||
use function get_bloginfo;
|
||||
use function get_locale;
|
||||
use function get_option;
|
||||
use function get_site_url;
|
||||
use function plugins_url;
|
||||
use function rest_url;
|
||||
use function wp_create_nonce;
|
||||
use function wp_json_encode;
|
||||
use function wp_parse_url;
|
||||
|
||||
/**
|
||||
* The Scan React initial state.
|
||||
*/
|
||||
class Initial_State {
|
||||
/**
|
||||
* Get the initial state data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_data() {
|
||||
$gmt_offset = get_option( 'gmt_offset' );
|
||||
$timezone_string = get_option( 'timezone_string' );
|
||||
$home_host = wp_parse_url( get_site_url(), PHP_URL_HOST );
|
||||
|
||||
return array(
|
||||
'API' => array(
|
||||
'WP_API_root' => esc_url_raw( rest_url() ),
|
||||
'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
),
|
||||
'jetpackStatus' => array(
|
||||
'calypsoSlug' => ( new Status() )->get_site_suffix(),
|
||||
),
|
||||
'siteData' => array(
|
||||
'id' => Jetpack_Options::get_option( 'id' ),
|
||||
'title' => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(),
|
||||
'adminUrl' => esc_url_raw( admin_url() ),
|
||||
'slug' => is_string( $home_host ) ? $home_host : '',
|
||||
'gmtOffset' => is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0,
|
||||
'timezoneString' => is_string( $timezone_string ) ? $timezone_string : '',
|
||||
'locale' => str_replace( '_', '-', (string) get_locale() ),
|
||||
),
|
||||
'assets' => array(
|
||||
'buildUrl' => plugins_url( '../build/', __FILE__ ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the initial state into a JavaScript variable.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render() {
|
||||
return 'var JPSCAN_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
|
||||
}
|
||||
}
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
/**
|
||||
* Primary class for the Jetpack Scan package.
|
||||
*
|
||||
* @package automattic/jetpack-scan-page
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Scan_Page;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
use Automattic\Jetpack\Admin_UI\Admin_Menu;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
|
||||
use function add_action;
|
||||
use function add_filter;
|
||||
use function apply_filters;
|
||||
use function call_user_func;
|
||||
use function current_user_can;
|
||||
use function did_action;
|
||||
use function do_action;
|
||||
use function function_exists;
|
||||
use function is_admin;
|
||||
use function is_multisite;
|
||||
use function remove_action;
|
||||
use function remove_all_actions;
|
||||
use function sanitize_text_field;
|
||||
use function wp_print_inline_script_tag;
|
||||
use function wp_scripts;
|
||||
use function wp_unslash;
|
||||
|
||||
/**
|
||||
* Class Jetpack_Scan
|
||||
*
|
||||
* Registers the Scan admin page and its REST routes inside the main
|
||||
* Jetpack plugin. The page bundle is built by `@wordpress/build`
|
||||
* (mirroring Newsletter / Forms); this class wires the wp-admin menu
|
||||
* + the bridges that route our user-facing slug to wp-build's
|
||||
* auto-generated enqueue / render functions.
|
||||
*/
|
||||
class Jetpack_Scan {
|
||||
|
||||
/**
|
||||
* URL-facing menu slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PAGE_SLUG = 'jetpack-scan';
|
||||
|
||||
/**
|
||||
* Internal slug emitted by `@wordpress/build` (`wpPlugin.pages[0]`
|
||||
* plus the `-wp-admin` suffix the build template appends). Used to
|
||||
* find the auto-generated render / enqueue functions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const WP_BUILD_SLUG = 'jetpack-scan-wp-admin';
|
||||
|
||||
/**
|
||||
* Filter name that gates the wp-build–based Scan dashboard.
|
||||
*
|
||||
* When this filter returns true, the new wp-admin Scan page is
|
||||
* registered and rendered. Default false during the modernization
|
||||
* roll-out — the package registers no admin menu and changes
|
||||
* nothing about the existing Jetpack UI when this filter is off.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MODERNIZATION_FILTER = 'rsm_jetpack_ui_modernization_scan';
|
||||
|
||||
/**
|
||||
* Entry point. Idempotent: safe to call from multiple bootstraps.
|
||||
*/
|
||||
public static function initialize() {
|
||||
if ( did_action( 'jetpack_scan_page_initialized' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! (bool) apply_filters( self::MODERNIZATION_FILTER, false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::load_wp_build();
|
||||
self::fix_boot_import_map_ordering();
|
||||
self::bridge_wp_build_enqueue();
|
||||
|
||||
add_action( 'admin_menu', array( __CLASS__, 'add_wp_admin_submenu' ) );
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
|
||||
add_filter( 'jetpack_package_versions', array( Package_Version::class, 'send_package_version_to_tracker' ) );
|
||||
|
||||
/**
|
||||
* Fires once the Jetpack Scan package has wired its hooks.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
do_action( 'jetpack_scan_page_initialized' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load wp-build generated registration files. Mirrors Newsletter / Forms.
|
||||
*/
|
||||
public static function load_wp_build() {
|
||||
// The polyfills force-replace core script handles (notably
|
||||
// `wp-private-apis`, a stateful singleton shared by every @wordpress
|
||||
// package on the page) during `wp_default_scripts`. Scope registration
|
||||
// to the Scan admin page so it never runs on other admin pages such as
|
||||
// the block editor. `$_GET['page']` is reliable this early — it's the
|
||||
// raw query param, available well before `current_screen` exists.
|
||||
if ( self::is_scan_admin_request() ) {
|
||||
WP_Build_Polyfills::register(
|
||||
'jetpack-scan',
|
||||
array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
|
||||
);
|
||||
}
|
||||
|
||||
$wp_build_index = dirname( __DIR__ ) . '/build/build.php';
|
||||
if ( file_exists( $wp_build_index ) ) {
|
||||
require_once $wp_build_index;
|
||||
}
|
||||
|
||||
// `page.php` ships an `admin_init` interceptor that takes over our
|
||||
// slug with a standalone (non-wp-admin) render. We want the
|
||||
// wp-admin integrated experience, so unregister it as soon as it's
|
||||
// loaded.
|
||||
remove_action(
|
||||
'admin_init',
|
||||
'jetpack_scan_jetpack_scan_intercept_render'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current request targets the Scan admin page.
|
||||
*
|
||||
* Used to scope the wp-build polyfill registration (which force-replaces
|
||||
* core script handles) to this one page, so it never affects other admin
|
||||
* pages. Reads the menu page slug directly so it is cheap and safe to call
|
||||
* at plugin-load time, before `current_screen` exists.
|
||||
*
|
||||
* @return bool True when serving the Scan page in wp-admin.
|
||||
*/
|
||||
public static function is_scan_admin_request() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return self::PAGE_SLUG === sanitize_text_field( wp_unslash( $_GET['page'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge wp-build's auto-generated enqueue function — which checks for
|
||||
* `?page=jetpack-scan-wp-admin` — to our user-facing slug
|
||||
* `?page=jetpack-scan`. Hooked at priority 9 so the wp-build copy
|
||||
* (registered at priority 10) sees the original `$_GET['page']` and
|
||||
* skips its own enqueue.
|
||||
*/
|
||||
public static function bridge_wp_build_enqueue() {
|
||||
add_action(
|
||||
'admin_enqueue_scripts',
|
||||
static function ( $hook_suffix ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['page'] ) || self::PAGE_SLUG !== $_GET['page'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enqueue_fn = 'jetpack_scan_jetpack_scan_wp_admin_enqueue_scripts';
|
||||
if ( ! function_exists( $enqueue_fn ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$original = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : null;
|
||||
$_GET['page'] = self::WP_BUILD_SLUG;
|
||||
// @phan-suppress-next-line PhanUndeclaredFunctionInCallable -- Function is generated by @wordpress/build into build/pages/jetpack-scan/page-wp-admin.php, which is outside Phan's analysis scope. The function_exists() guard above protects the call at runtime.
|
||||
call_user_func( $enqueue_fn, $hook_suffix );
|
||||
if ( null === $original ) {
|
||||
unset( $_GET['page'] );
|
||||
} else {
|
||||
$_GET['page'] = $original;
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
},
|
||||
9
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix import map ordering for the wp-build boot script.
|
||||
*
|
||||
* In wp-admin, `_wp_footer_scripts` (classic scripts) and
|
||||
* `print_import_map` both hook into `admin_print_footer_scripts` at
|
||||
* priority 10, but `_wp_footer_scripts` is registered first. This
|
||||
* causes the inline `import("@wordpress/boot")` to execute before
|
||||
* the import map exists.
|
||||
*
|
||||
* This fix moves the import() call from the classic inline script to
|
||||
* a `<script type="module">` printed at priority 20 (after the import
|
||||
* map).
|
||||
*
|
||||
* @todo Remove once @wordpress/build ships with the loader.js fix
|
||||
* upstream (WordPress/gutenberg#76870) and Jetpack updates the
|
||||
* dependency.
|
||||
*/
|
||||
public static function fix_boot_import_map_ordering() {
|
||||
$handle = self::WP_BUILD_SLUG . '-prerequisites';
|
||||
|
||||
add_action(
|
||||
'admin_enqueue_scripts',
|
||||
static function () use ( $handle ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['page'] ) || self::PAGE_SLUG !== $_GET['page'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = wp_scripts()->get_data( $handle, 'after' );
|
||||
if ( empty( $data ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$boot_script = null;
|
||||
$remaining = array();
|
||||
foreach ( $data as $line ) {
|
||||
if ( strpos( $line, '@wordpress/boot' ) !== false ) {
|
||||
$boot_script = $line;
|
||||
} else {
|
||||
$remaining[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
if ( null === $boot_script ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_scripts()->add_data( $handle, 'after', $remaining );
|
||||
|
||||
add_action(
|
||||
'admin_print_footer_scripts',
|
||||
static function () use ( $boot_script ) {
|
||||
wp_print_inline_script_tag( $boot_script, array( 'type' => 'module' ) );
|
||||
},
|
||||
20
|
||||
);
|
||||
},
|
||||
PHP_INT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Scan submenu under Jetpack.
|
||||
*
|
||||
* @return string|null The resulting page's hook suffix, if registered.
|
||||
*/
|
||||
public static function add_wp_admin_submenu() {
|
||||
if ( ! self::is_available() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$render_fn = 'jetpack_scan_jetpack_scan_wp_admin_render_page';
|
||||
$render = function_exists( $render_fn )
|
||||
? $render_fn
|
||||
: array( __CLASS__, 'render_page_fallback' );
|
||||
|
||||
$page_suffix = Admin_Menu::add_menu(
|
||||
/** "Scan" is a product name, do not translate. */
|
||||
'Scan',
|
||||
'Scan',
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
$render,
|
||||
6
|
||||
);
|
||||
|
||||
if ( $page_suffix ) {
|
||||
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
|
||||
}
|
||||
|
||||
return $page_suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Scan page should be shown to the current user.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_available() {
|
||||
if ( is_multisite() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ( new Connection_Manager() )->is_user_connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the admin page is loaded.
|
||||
*
|
||||
* Silences the standard wp-admin notice channels so JITMs and
|
||||
* plugin-update messages don't reflow the focused Scan layout
|
||||
* mid-scan or while a fix modal is open.
|
||||
*/
|
||||
public static function admin_init() {
|
||||
remove_all_actions( 'admin_notices' );
|
||||
remove_all_actions( 'all_admin_notices' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback render — used only if the wp-build registration file
|
||||
* isn't loaded (e.g. the package wasn't built yet). Renders a bare
|
||||
* mount node so the page doesn't 500 in dev.
|
||||
*/
|
||||
public static function render_page_fallback() {
|
||||
?>
|
||||
<div id="jetpack-scan-page-root"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the REST routes backing the Scan UI.
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
REST_Controller::register_rest_routes();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*
|
||||
* @package automattic/jetpack-scan-page
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Scan_Page;
|
||||
|
||||
/**
|
||||
* The Package_Version class.
|
||||
*
|
||||
* Does *not* use namespaced versioning ("VXXXX") because send_package_version_to_tracker() is used as a
|
||||
* "jetpack_package_versions" filter, and said filter gets run during a plugin upgrade, so it always expects to
|
||||
* find the "Package_Version" class with the same namespace, name, and interface.
|
||||
*/
|
||||
class Package_Version {
|
||||
|
||||
const PACKAGE_VERSION = '0.1.8';
|
||||
|
||||
const PACKAGE_SLUG = 'scan-page';
|
||||
|
||||
/**
|
||||
* Adds the package slug and version to the package version tracker's data.
|
||||
*
|
||||
* @param array $package_versions The package version array.
|
||||
*
|
||||
* @return array The package version array.
|
||||
*/
|
||||
public static function send_package_version_to_tracker( $package_versions ) {
|
||||
$package_versions[ self::PACKAGE_SLUG ] = self::PACKAGE_VERSION;
|
||||
|
||||
return $package_versions;
|
||||
}
|
||||
}
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
/**
|
||||
* The Scan REST Controller.
|
||||
*
|
||||
* Registers the `/jetpack/v4/site/scan/*` routes backing the admin UI.
|
||||
* Read routes proxy to the corresponding WPCOM v2 endpoint, mutation
|
||||
* routes proxy to the user-scoped `wpcom/v2 /sites/:siteId/alerts/*`
|
||||
* surface that the Calypso Scan dashboard already uses.
|
||||
*
|
||||
* @package automattic/jetpack-scan-page
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Scan_Page;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Status\Visitor;
|
||||
use Jetpack_Options;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Server;
|
||||
use function add_query_arg;
|
||||
use function current_user_can;
|
||||
use function esc_html__;
|
||||
use function is_wp_error;
|
||||
use function json_decode;
|
||||
use function register_rest_route;
|
||||
use function rest_ensure_response;
|
||||
use function wp_json_encode;
|
||||
use function wp_remote_retrieve_body;
|
||||
use function wp_remote_retrieve_response_code;
|
||||
|
||||
/**
|
||||
* REST routes for the Scan UI.
|
||||
*/
|
||||
class REST_Controller {
|
||||
|
||||
/**
|
||||
* REST namespace used by this package.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REST_NAMESPACE = 'jetpack/v4';
|
||||
|
||||
/**
|
||||
* REST route prefix used by this package.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REST_ROUTE_PREFIX = 'site/scan';
|
||||
|
||||
/**
|
||||
* Register the REST routes backing the Scan UI.
|
||||
*
|
||||
* Read paths land in Phase 1 (this controller). Mutation paths
|
||||
* (threat-id `ignore` / `unignore`, bulk `threats/fix`,
|
||||
* `threats/fix-status`) land in Phase 3. The scan-enqueue mutation
|
||||
* lands in Phase 5.
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX,
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_site_scan' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/history',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_site_scan_history' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/counts',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_site_scan_counts' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/threat/(?P<id>[\w\-]+)/ignore',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( __CLASS__, 'post_threat_ignore' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/threat/(?P<id>[\w\-]+)/unignore',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( __CLASS__, 'post_threat_unignore' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/threats/fix',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( __CLASS__, 'post_threats_fix' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'threat_ids' => array(
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/enqueue',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( __CLASS__, 'post_scan_enqueue' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/' . self::REST_ROUTE_PREFIX . '/threats/fix-status',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_threats_fix_status' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
'args' => array(
|
||||
'threat_ids' => array(
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback: admin-only. Mirrors the gate in
|
||||
* `Jetpack_Scan::is_available()`.
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function permissions_check() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
esc_html__( 'You do not have permission to access this resource.', 'jetpack-scan-page' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /site/scan — current scan state + active threats.
|
||||
*
|
||||
* Proxies WPCOM `/sites/:siteId/scan` with blog auth (matches Protect
|
||||
* plugin's `Threats::fetch_status()`).
|
||||
*
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function get_site_scan() {
|
||||
return self::proxy_get( '/scan', 'scan', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /site/scan/history — past scan runs and their threats.
|
||||
*
|
||||
* Proxies WPCOM `/sites/:siteId/scan/history` with blog auth (matches
|
||||
* Protect plugin's `Threats::history()`).
|
||||
*
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function get_site_scan_history() {
|
||||
return self::proxy_get( '/scan/history', 'scan_history', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /site/scan/counts — threat counts for the overview tabs.
|
||||
*
|
||||
* Proxies WPCOM `/sites/:siteId/scan/counts` with blog auth.
|
||||
*
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function get_site_scan_counts() {
|
||||
return self::proxy_get( '/scan/counts', 'scan_counts', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /site/scan/threat/{id}/ignore — mark a threat as ignored.
|
||||
*
|
||||
* Proxies WPCOM `POST /sites/:siteId/alerts/:threatId` with
|
||||
* `{ ignore: true }`. Same shape Protect plugin's
|
||||
* `Threats::ignore_threat()` already uses.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function post_threat_ignore( WP_REST_Request $request ) {
|
||||
$threat_id = (string) $request->get_param( 'id' );
|
||||
return self::proxy_post(
|
||||
sprintf( '/alerts/%s', rawurlencode( $threat_id ) ),
|
||||
array( 'ignore' => true ),
|
||||
'scan_threat_ignore'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /site/scan/threat/{id}/unignore — reactivate a previously-ignored
|
||||
* threat.
|
||||
*
|
||||
* Proxies WPCOM `POST /sites/:siteId/alerts/:threatId` with
|
||||
* `{ unignore: true }`.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function post_threat_unignore( WP_REST_Request $request ) {
|
||||
$threat_id = (string) $request->get_param( 'id' );
|
||||
return self::proxy_post(
|
||||
sprintf( '/alerts/%s', rawurlencode( $threat_id ) ),
|
||||
array( 'unignore' => true ),
|
||||
'scan_threat_unignore'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /site/scan/threats/fix — kick auto-fix for one or more threats.
|
||||
*
|
||||
* Proxies WPCOM `POST /sites/:siteId/alerts/fix` with
|
||||
* `{ threat_ids: [...] }`. Same endpoint handles single + bulk fix.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function post_threats_fix( WP_REST_Request $request ) {
|
||||
$ids = (array) $request->get_param( 'threat_ids' );
|
||||
$ids = array_values( array_filter( array_map( 'strval', $ids ) ) );
|
||||
return self::proxy_post(
|
||||
'/alerts/fix',
|
||||
array( 'threat_ids' => $ids ),
|
||||
'scan_threats_fix'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /site/scan/enqueue — trigger an immediate scan run.
|
||||
*
|
||||
* Proxies WPCOM `POST /sites/:siteId/scan/enqueue` with blog auth
|
||||
* (matches Protect plugin's `Threats::scan()`).
|
||||
*
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function post_scan_enqueue() {
|
||||
return self::proxy_post( '/scan/enqueue', array(), 'scan_enqueue', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /site/scan/threats/fix-status — poll the auto-fixer for the
|
||||
* current state of one or more threats.
|
||||
*
|
||||
* Proxies WPCOM `GET /sites/:siteId/alerts/fix?threat_ids[]=…`. Body
|
||||
* shape mirrors `post_threats_fix` so the UI hook can poll until each
|
||||
* threat reaches a terminal state.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
public static function get_threats_fix_status( WP_REST_Request $request ) {
|
||||
$ids = (array) $request->get_param( 'threat_ids' );
|
||||
$ids = array_values( array_filter( array_map( 'strval', $ids ) ) );
|
||||
|
||||
$path = add_query_arg( array( 'threat_ids' => $ids ), '/alerts/fix' );
|
||||
return self::proxy_get( $path, 'scan_threats_fix_status' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy a GET request to the WPCOM v2 Scan endpoint and pass the JSON
|
||||
* body through (or surface a WP_Error mapping the upstream status
|
||||
* code).
|
||||
*
|
||||
* Site-level reads (`/scan`, `/scan/history`, `/scan/counts`) sign
|
||||
* with blog auth — the same contract Protect plugin's `Threats::*`
|
||||
* helpers use, and what WPCOM expects for these endpoints. Alert /
|
||||
* fix-status endpoints stay on user auth so per-user permissions on
|
||||
* threat mutations carry through.
|
||||
*
|
||||
* Forwarding the visitor IP keeps WPCOM-side audit logs aligned with
|
||||
* the existing `/jetpack/v4/site/activity` proxy in `activity-log`.
|
||||
*
|
||||
* @param string $upstream_path WPCOM path suffix (e.g. `/scan`, `/alerts/fix`).
|
||||
* @param string $error_slug Slug used when synthesising WP_Error codes.
|
||||
* @param bool $as_blog Sign with blog auth instead of user auth.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
private static function proxy_get( $upstream_path, $error_slug, $as_blog = false ) {
|
||||
$path = self::resolve_blog_path( $upstream_path );
|
||||
if ( is_wp_error( $path ) ) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'method' => 'GET',
|
||||
'headers' => array(
|
||||
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
|
||||
),
|
||||
);
|
||||
|
||||
$response = $as_blog
|
||||
? Client::wpcom_json_api_request_as_blog(
|
||||
$path,
|
||||
'2',
|
||||
$args,
|
||||
null,
|
||||
'wpcom'
|
||||
)
|
||||
: Client::wpcom_json_api_request_as_user(
|
||||
$path,
|
||||
'2',
|
||||
$args,
|
||||
null,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
return self::map_response( $response, $error_slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy a POST request to the user-scoped WPCOM v2 Scan endpoint
|
||||
* with a JSON body and pass the response through (or surface a
|
||||
* WP_Error mapping the upstream status code).
|
||||
*
|
||||
* @param string $upstream_path WPCOM path suffix (e.g. `/alerts/fix`).
|
||||
* @param array $body Body payload sent as JSON.
|
||||
* @param string $error_slug Slug used when synthesising WP_Error codes.
|
||||
* @param bool $as_blog Sign with blog auth instead of user auth.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
private static function proxy_post( $upstream_path, array $body, $error_slug, $as_blog = false ) {
|
||||
$path = self::resolve_blog_path( $upstream_path );
|
||||
if ( is_wp_error( $path ) ) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
|
||||
),
|
||||
);
|
||||
$encoded_body = wp_json_encode( $body, JSON_UNESCAPED_SLASHES );
|
||||
|
||||
$response = $as_blog
|
||||
? Client::wpcom_json_api_request_as_blog(
|
||||
$path,
|
||||
'2',
|
||||
$args,
|
||||
$encoded_body,
|
||||
'wpcom'
|
||||
)
|
||||
: Client::wpcom_json_api_request_as_user(
|
||||
$path,
|
||||
'2',
|
||||
$args,
|
||||
$encoded_body,
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
return self::map_response( $response, $error_slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the connected blog id and prefix it onto the WPCOM path
|
||||
* suffix. Surfaces a 400 WP_Error if the site isn't connected.
|
||||
*
|
||||
* @param string $upstream_path WPCOM path suffix.
|
||||
* @return string|WP_Error
|
||||
*/
|
||||
private static function resolve_blog_path( $upstream_path ) {
|
||||
$blog_id = (int) Jetpack_Options::get_option( 'id' );
|
||||
if ( $blog_id <= 0 ) {
|
||||
return new WP_Error(
|
||||
'jetpack_scan_no_blog_id',
|
||||
esc_html__( 'Site is not connected to WordPress.com.', 'jetpack-scan-page' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf( '/sites/%d%s', $blog_id, $upstream_path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a WPCOM HTTP response into a `WP_REST_Response` /
|
||||
* `WP_Error` for the local `/jetpack/v4/*` route to return.
|
||||
*
|
||||
* @param array|WP_Error $response Result of `wpcom_json_api_request_as_user`.
|
||||
* @param string $error_slug Slug used when synthesising WP_Error codes.
|
||||
* @return \WP_REST_Response|WP_Error
|
||||
*/
|
||||
private static function map_response( $response, $error_slug ) {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'jetpack_' . $error_slug . '_request_failed',
|
||||
$response->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
$status = (int) wp_remote_retrieve_response_code( $response );
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( $status < 200 || $status >= 300 ) {
|
||||
return new WP_Error(
|
||||
'jetpack_' . $error_slug . '_request_failed',
|
||||
isset( $body['message'] ) ? (string) $body['message'] : esc_html__( 'Unable to fetch Scan data.', 'jetpack-scan-page' ),
|
||||
array( 'status' => $status > 0 ? $status : 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $body );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user