PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.4
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-onboarding.php

class-onboarding.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.4, at includes/class-onboarding.php

276 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * First-run onboarding wizard.
4 *
5 * Owns:
6 * - The hidden submenu page (`xspeed-onboarding`) and its single root <div>.
7 * - The activation-triggered redirect to that page.
8 * - The completion flag.
9 * - The environment-check payload + REST routes (`/onboarding/apply`,
10 * `/onboarding/complete`).
11 *
12 * Behavior contract is documented in DESIGN.md §25.
13 *
14 * @package XSpeed
15 */
16
17 namespace XSpeed;
18
19 defined( 'ABSPATH' ) || exit;
20
21 class Onboarding {
22
23 const PAGE_SLUG = 'xspeed-onboarding';
24 const OPT_REDIRECT = 'xspeed_redirect_to_onboarding';
25 const OPT_COMPLETE = 'xspeed_onboarding_complete';
26
27 public function __construct() {
28 add_action( 'admin_menu', array( $this, 'register_menu' ), 20 );
29 add_action( 'admin_init', array( $this, 'maybe_redirect' ) );
30 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
31 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
32 }
33
34 /**
35 * Mark the next admin_init to redirect this user to the wizard. Called
36 * from Plugin::activate(). Idempotent.
37 */
38 public static function flag_redirect() {
39 update_option( self::OPT_REDIRECT, 1, false );
40 }
41
42 public static function is_complete() {
43 return (bool) get_option( self::OPT_COMPLETE, 0 );
44 }
45
46 public function register_menu() {
47 // Visible "Setup Wizard" submenu under xSpeed. Stays in the
48 // menu even after onboarding is complete so users can re-run
49 // the wizard any time (a fresh run sets
50 // xspeed_onboarding_complete = false again on completion).
51 add_submenu_page(
52 Admin::PAGE_SLUG,
53 __( 'xSpeed Setup Wizard', 'xspeed' ),
54 __( 'Setup Wizard', 'xspeed' ),
55 'manage_options',
56 self::PAGE_SLUG,
57 array( $this, 'render' )
58 );
59 }
60
61 public function render() {
62 // Reuse the dashboard's mount ID so the Tailwind `important` selector
63 // keeps working (utilities are scoped to `#xspeed-app`). The host
64 // class strips the dashboard-only fixed-height/flex shell so the
65 // wizard can lay out as a centered card. See src/styles.css.
66 $dark = 'dark' === Admin::user_theme() ? ' dark' : '';
67 printf(
68 '<div id="xspeed-app" class="xspeed-root xspeed-onboarding-host%s"></div>',
69 esc_attr( $dark )
70 );
71 }
72
73 /**
74 * Send the user to the wizard on the first admin_init after activation.
75 * Skipped for AJAX/REST/cron, bulk-activate flows, and when the wizard
76 * has already been completed.
77 */
78 public function maybe_redirect() {
79 if ( ! get_option( self::OPT_REDIRECT ) ) {
80 return;
81 }
82 if ( wp_doing_ajax() || wp_doing_cron() ) {
83 return;
84 }
85 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing decision; we don't process form data here.
86 if ( isset( $_GET['activate-multi'] ) ) {
87 delete_option( self::OPT_REDIRECT );
88 return;
89 }
90 if ( ! current_user_can( 'manage_options' ) ) {
91 return;
92 }
93 if ( self::is_complete() ) {
94 delete_option( self::OPT_REDIRECT );
95 return;
96 }
97
98 delete_option( self::OPT_REDIRECT );
99
100 wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) );
101 exit;
102 }
103
104 public function enqueue( $hook ) {
105 // `add_submenu_page` registers this page's hook suffix as
106 // "{parent_slug}_page_{slug}", but because we immediately call
107 // `remove_submenu_page`, WordPress still loads it via the toplevel
108 // page hook when the URL is hit directly. Match both shapes.
109 $wanted = array(
110 'admin_page_' . self::PAGE_SLUG,
111 Admin::PAGE_SLUG . '_page_' . self::PAGE_SLUG,
112 'toplevel_page_' . self::PAGE_SLUG,
113 );
114 if ( ! in_array( $hook, $wanted, true ) ) {
115 return;
116 }
117
118 $asset_js = XSPEED_DIR . 'assets/admin.js';
119 $asset_css = XSPEED_DIR . 'assets/admin.css';
120
121 if ( file_exists( $asset_js ) ) {
122 wp_enqueue_script(
123 'xspeed-admin',
124 XSPEED_URL . 'assets/admin.js',
125 array( 'wp-api-fetch' ),
126 XSPEED_VERSION,
127 true
128 );
129 }
130 if ( file_exists( $asset_css ) ) {
131 wp_enqueue_style(
132 'xspeed-admin',
133 XSPEED_URL . 'assets/admin.css',
134 array(),
135 XSPEED_VERSION
136 );
137 }
138
139 wp_localize_script(
140 'xspeed-admin',
141 'XSpeedConfig',
142 array(
143 'mode' => 'onboarding',
144 'restUrl' => esc_url_raw( rest_url( Rest_Api::NAMESPACE_V1 ) ),
145 'nonce' => wp_create_nonce( 'wp_rest' ),
146 'version' => XSPEED_VERSION,
147 'dashboardUrl' => admin_url( 'admin.php?page=' . Admin::PAGE_SLUG ),
148 'bootstrap' => array(
149 'settings' => Settings::get(),
150 'env' => self::env_payload(),
151 ),
152 )
153 );
154 }
155
156 /**
157 * Environment snapshot rendered as Step 1's health rows. Pure read —
158 * never writes to disk, never makes outbound requests.
159 */
160 public static function env_payload() {
161 // Single source of truth for environment checks lives in
162 // XSpeed\Health. Both the Onboarding wizard and the Health
163 // module's dashboard panel consume from there.
164 return Health::env_payload();
165 }
166
167 public function register_routes() {
168 register_rest_route(
169 Rest_Api::NAMESPACE_V1,
170 '/onboarding/apply',
171 array(
172 'methods' => 'POST',
173 'callback' => array( $this, 'apply' ),
174 'permission_callback' => array( $this, 'permissions' ),
175 )
176 );
177 register_rest_route(
178 Rest_Api::NAMESPACE_V1,
179 '/onboarding/complete',
180 array(
181 'methods' => 'POST',
182 'callback' => array( $this, 'complete' ),
183 'permission_callback' => array( $this, 'permissions' ),
184 )
185 );
186 register_rest_route(
187 Rest_Api::NAMESPACE_V1,
188 '/onboarding/reset',
189 array(
190 'methods' => 'POST',
191 'callback' => array( $this, 'reset' ),
192 'permission_callback' => array( $this, 'permissions' ),
193 )
194 );
195 }
196
197 public function permissions() {
198 return current_user_can( 'manage_options' );
199 }
200
201 /**
202 * Apply the wizard's selected settings + flip the cache drop-in on if
203 * requested. Single REST round-trip so the wizard never lands in a
204 * half-applied state.
205 */
206 public function apply( \WP_REST_Request $request ) {
207 $params = $request->get_json_params();
208 if ( ! is_array( $params ) ) {
209 $params = $request->get_params();
210 }
211
212 $want_cache = ! empty( $params['cache_enabled'] );
213
214 // Per-module settings go through Settings_Manager (the schema-
215 // validated authority for each module). Legacy Settings::update
216 // is reserved for fields still in xspeed_options (cache_expiry,
217 // excluded_urls) until the Cache module migration lands.
218 Settings_Manager::update(
219 'minify',
220 array(
221 'minify_html' => ! empty( $params['minify_html'] ),
222 'minify_css' => ! empty( $params['minify_css'] ),
223 'minify_js' => ! empty( $params['minify_js'] ),
224 'defer_js' => ! empty( $params['defer_js'] ),
225 )
226 );
227 Settings_Manager::update(
228 'gzip',
229 array(
230 'gzip_enabled' => ! empty( $params['gzip_enabled'] ),
231 )
232 );
233 Settings_Manager::update(
234 'cache',
235 array(
236 'cache_expiry' => isset( $params['cache_expiry'] ) ? absint( $params['cache_expiry'] ) : 24,
237 )
238 );
239 // New optional modules surfaced in the wizard — keys are
240 // only updated when present in the payload so legacy
241 // onboarding-complete sites aren't disturbed.
242 if ( array_key_exists( 'lazy_images', $params ) ) {
243 Settings_Manager::update(
244 'lazy',
245 array( 'lazy_images' => ! empty( $params['lazy_images'] ) )
246 );
247 }
248 if ( array_key_exists( 'browser_cache', $params ) ) {
249 Settings_Manager::update(
250 'browser-cache',
251 array( 'enabled' => ! empty( $params['browser_cache'] ) )
252 );
253 }
254
255 $install_state = Cache::toggle( $want_cache );
256 Settings::update( array( 'cache_enabled' => $install_state['enabled'] ) );
257
258 return rest_ensure_response(
259 array(
260 'settings' => Settings::get(),
261 'install_state' => $install_state,
262 )
263 );
264 }
265
266 public function complete() {
267 update_option( self::OPT_COMPLETE, 1, false );
268 return rest_ensure_response( array( 'ok' => true ) );
269 }
270
271 public function reset() {
272 delete_option( self::OPT_COMPLETE );
273 return rest_ensure_response( array( 'ok' => true ) );
274 }
275 }
276