PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.7
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.7, at includes/class-onboarding.php

307 lines 9.6 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 // Use the white-label brand in the page <title> so an agency's
52 // rebrand carries through to the wizard tab/title, not just the
53 // dashboard. (FBS white-label-onboarding)
54 $brand = Admin::branding();
55 /* translators: %s: brand name (xSpeed by default, or the white-label name). */
56 $page_title = sprintf( __( '%s Setup Wizard', 'xspeed' ), $brand['name'] );
57 add_submenu_page(
58 Admin::PAGE_SLUG,
59 $page_title,
60 __( 'Setup Wizard', 'xspeed' ),
61 'manage_options',
62 self::PAGE_SLUG,
63 array( $this, 'render' )
64 );
65 }
66
67 public function render() {
68 // Reuse the dashboard's mount ID so the Tailwind `important` selector
69 // keeps working (utilities are scoped to `#xspeed-app`). The host
70 // class strips the dashboard-only fixed-height/flex shell so the
71 // wizard can lay out as a centered card. See src/styles.css.
72 $dark = 'dark' === Admin::user_theme() ? ' dark' : '';
73 printf(
74 '<div id="xspeed-app" class="xspeed-root xspeed-onboarding-host%s"></div>',
75 esc_attr( $dark )
76 );
77 }
78
79 /**
80 * Send the user to the wizard on the first admin_init after activation.
81 * Skipped for AJAX/REST/cron, bulk-activate flows, and when the wizard
82 * has already been completed.
83 */
84 public function maybe_redirect() {
85 if ( ! get_option( self::OPT_REDIRECT ) ) {
86 return;
87 }
88 if ( wp_doing_ajax() || wp_doing_cron() ) {
89 return;
90 }
91 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing decision; we don't process form data here.
92 if ( isset( $_GET['activate-multi'] ) ) {
93 delete_option( self::OPT_REDIRECT );
94 return;
95 }
96 if ( ! current_user_can( 'manage_options' ) ) {
97 return;
98 }
99 if ( self::is_complete() ) {
100 delete_option( self::OPT_REDIRECT );
101 return;
102 }
103
104 delete_option( self::OPT_REDIRECT );
105
106 wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) );
107 exit;
108 }
109
110 public function enqueue( $hook ) {
111 unset( $hook );
112 // Gate on the page SLUG, not the admin hook suffix. WordPress derives
113 // the submenu hook from the *sanitized parent menu title*, so when the
114 // White-Label module renames the menu (e.g. "AcmeSpeed") the hook
115 // becomes "acmespeed_page_xspeed-onboarding" and any check built on
116 // Admin::PAGE_SLUG ("xspeed_page_…") silently stops matching — the
117 // wizard bundle then never enqueues and the page renders blank.
118 // The ?page= slug is brand-independent, so match on that instead.
119 // (FBS-82222)
120 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen gate, no state change.
121 if ( self::PAGE_SLUG !== $page ) {
122 return;
123 }
124
125 $asset_js = XSPEED_DIR . 'assets/admin.js';
126 $asset_css = XSPEED_DIR . 'assets/admin.css';
127
128 if ( file_exists( $asset_js ) ) {
129 wp_enqueue_script(
130 'xspeed-admin',
131 XSPEED_URL . 'assets/admin.js',
132 array( 'wp-api-fetch' ),
133 XSPEED_VERSION,
134 true
135 );
136 }
137 if ( file_exists( $asset_css ) ) {
138 wp_enqueue_style(
139 'xspeed-admin',
140 XSPEED_URL . 'assets/admin.css',
141 array(),
142 XSPEED_VERSION
143 );
144 }
145
146 wp_localize_script(
147 'xspeed-admin',
148 'XSpeedConfig',
149 array(
150 'mode' => 'onboarding',
151 'restUrl' => esc_url_raw( rest_url( Rest_Api::NAMESPACE_V1 ) ),
152 'nonce' => wp_create_nonce( 'wp_rest' ),
153 'version' => XSPEED_VERSION,
154 // White-label branding must reach the wizard too — without it
155 // the onboarding chrome shows the default "xSpeed" even when an
156 // agency has rebranded. (FBS white-label-onboarding)
157 'branding' => Admin::branding(),
158 'dashboardUrl' => admin_url( 'admin.php?page=' . Admin::PAGE_SLUG ),
159 'bootstrap' => array(
160 'settings' => Settings::get(),
161 'env' => self::env_payload(),
162 ),
163 )
164 );
165 }
166
167 /**
168 * Environment snapshot rendered as Step 1's health rows. Pure read —
169 * never writes to disk, never makes outbound requests.
170 */
171 public static function env_payload() {
172 // Single source of truth for environment checks lives in
173 // XSpeed\Health. Both the Onboarding wizard and the Health
174 // module's dashboard panel consume from there.
175 return Health::env_payload();
176 }
177
178 public function register_routes() {
179 register_rest_route(
180 Rest_Api::NAMESPACE_V1,
181 '/onboarding/apply',
182 array(
183 'methods' => 'POST',
184 'callback' => array( $this, 'apply' ),
185 'permission_callback' => array( $this, 'permissions' ),
186 )
187 );
188 register_rest_route(
189 Rest_Api::NAMESPACE_V1,
190 '/onboarding/complete',
191 array(
192 'methods' => 'POST',
193 'callback' => array( $this, 'complete' ),
194 'permission_callback' => array( $this, 'permissions' ),
195 )
196 );
197 register_rest_route(
198 Rest_Api::NAMESPACE_V1,
199 '/onboarding/reset',
200 array(
201 'methods' => 'POST',
202 'callback' => array( $this, 'reset' ),
203 'permission_callback' => array( $this, 'permissions' ),
204 )
205 );
206 }
207
208 public function permissions() {
209 return current_user_can( 'manage_options' );
210 }
211
212 /**
213 * Apply the wizard's selected settings + flip the cache drop-in on if
214 * requested. Single REST round-trip so the wizard never lands in a
215 * half-applied state.
216 */
217 public function apply( \WP_REST_Request $request ) {
218 $params = $request->get_json_params();
219 if ( ! is_array( $params ) ) {
220 $params = $request->get_params();
221 }
222
223 $want_cache = ! empty( $params['cache_enabled'] );
224
225 // Per-module settings go through Settings_Manager (the schema-
226 // validated authority for each module). Legacy Settings::update
227 // is reserved for fields still in xspeed_options (cache_expiry,
228 // excluded_urls) until the Cache module migration lands.
229 Settings_Manager::update(
230 'minify',
231 array(
232 'minify_html' => ! empty( $params['minify_html'] ),
233 'minify_css' => ! empty( $params['minify_css'] ),
234 'minify_js' => ! empty( $params['minify_js'] ),
235 'defer_js' => ! empty( $params['defer_js'] ),
236 )
237 );
238 Settings_Manager::update(
239 'gzip',
240 array(
241 'gzip_enabled' => ! empty( $params['gzip_enabled'] ),
242 )
243 );
244 Settings_Manager::update(
245 'cache',
246 array(
247 'cache_expiry' => isset( $params['cache_expiry'] ) ? absint( $params['cache_expiry'] ) : 24,
248 )
249 );
250 // New optional modules surfaced in the wizard — keys are
251 // only updated when present in the payload so legacy
252 // onboarding-complete sites aren't disturbed.
253 if ( array_key_exists( 'lazy_images', $params ) ) {
254 Settings_Manager::update(
255 'lazy',
256 array( 'lazy_images' => ! empty( $params['lazy_images'] ) )
257 );
258 }
259 if ( array_key_exists( 'browser_cache', $params ) ) {
260 Settings_Manager::update(
261 'browser-cache',
262 array( 'enabled' => ! empty( $params['browser_cache'] ) )
263 );
264 }
265
266 // Opt-in usage analytics. Only acted on when the key is present in the
267 // payload (legacy onboarding-complete sites are left untouched). The
268 // consent toggle defaults OFF in the wizard, so the common path is
269 // usage_tracking=false → tracker stays dormant, no outbound HTTP.
270 if ( array_key_exists( 'usage_tracking', $params ) ) {
271 $tracker = Plugin::instance()->usage_tracker();
272 if ( $tracker ) {
273 $tracker->opt_in( ! empty( $params['usage_tracking'] ) );
274 }
275 }
276
277 $install_state = Cache::toggle( $want_cache );
278 Settings::update( array( 'cache_enabled' => $install_state['enabled'] ) );
279
280 // Recompute the unified nginx block AFTER cache_enabled is persisted.
281 // Cache::toggle() computes it inline, before the Settings::update()
282 // above writes cache_enabled — so the block in $install_state reflects
283 // the PRE-apply state (CacheModule::nginx_directives() gates on
284 // cache_enabled). Regenerate so the wizard's Done step shows the
285 // snippet for the configuration the user just applied. Mirrors the
286 // same fix in Rest_Api::toggle_cache().
287 $install_state['nginx_server_block'] = Cache::full_nginx_server_block();
288
289 return rest_ensure_response(
290 array(
291 'settings' => Settings::get(),
292 'install_state' => $install_state,
293 )
294 );
295 }
296
297 public function complete() {
298 update_option( self::OPT_COMPLETE, 1, false );
299 return rest_ensure_response( array( 'ok' => true ) );
300 }
301
302 public function reset() {
303 delete_option( self::OPT_COMPLETE );
304 return rest_ensure_response( array( 'ok' => true ) );
305 }
306 }
307