PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-setup-wizard / first-run-launcher / class-wpbc-setup-wizard-first-install-state.php

class-wpbc-setup-wizard-first-install-state.php in Booking Calendar 11.9, at includes/page-setup-wizard/first-run-launcher/class-wpbc-setup-wizard-first-install-state.php

348 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * First-install state and activation-intent contracts for Setup Wizard.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 require_once dirname( __DIR__, 2 ) . '/_functions/class-wpbc-environment-policy.php';
13
14 /**
15 * Own immutable first-install markers and versioned activation redirect intent.
16 *
17 * Genuine-install detection runs before activation creates tables and options.
18 * This class converts that request-scoped result into a small transient payload
19 * which the following administration request can route without re-detecting an
20 * already-mutated installation.
21 */
22 final class WPBC_Setup_Wizard_First_Install_State {
23
24 /** Current activation redirect payload version. */
25 const REDIRECT_SCHEMA_VERSION = 2;
26
27 /** Schema version for updater-to-runtime navigation handoff records. */
28 const UPDATE_CONTEXT_SCHEMA_VERSION = 1;
29
30 /** Setup Wizard-owned transient containing the structured activation intent. */
31 const ACTIVATION_INTENT_TRANSIENT = '_booking_setup_wizard_activation_redirect_intent';
32
33 /** Lifetime of immediate activation navigation state, matching the released redirect contract. */
34 const ACTIVATION_INTENT_TTL = 30;
35
36 /** Lifetime of a manual-update redirect waiting for the initiating administrator. */
37 const MANUAL_UPDATE_INTENT_TTL = 3600;
38
39 /** Maximum age of an updater-to-runtime handoff record. */
40 const UPDATE_CONTEXT_TTL = 3600;
41
42 /** Redirect destination for a genuine first installation. */
43 const DESTINATION_SETUP_WIZARD = 'setup_wizard';
44
45 /** Legacy first-install destination accepted during the 11.9 development transition. */
46 const DESTINATION_BOOKING_LISTING = 'booking_listing_setup_prompt';
47
48 /** Redirect destination for an update or reactivation. */
49 const DESTINATION_WHATS_NEW = 'whats_new';
50
51 /** Persistent option carrying updater context across replaced plugin files. */
52 const UPDATE_CONTEXT_OPTION = 'booking_update_navigation_context';
53
54 /** Interactive single-plugin update source. */
55 const UPDATE_SOURCE_MANUAL = 'manual_single_plugin_update';
56
57 /** Automatic, command-line, or otherwise non-interactive update source. */
58 const UPDATE_SOURCE_SUPPRESSED = 'non_interactive_update';
59
60 /**
61 * Check the activation-owned persistent genuine-install marker.
62 *
63 * @return bool True only for a site initialized by the genuine first-install path.
64 */
65 public static function is_initial_install_site() {
66 return 'On' === get_bk_option( 'booking_setup_wizard_initial_install' );
67 }
68
69 /**
70 * Build the transient redirect intent before activation mutates install state.
71 *
72 * @param bool $is_initial_install Whether activation began from an empty installation.
73 * @param string $plugin_version Booking Calendar version being activated.
74 * @param string $source Request source used for diagnostics and routing.
75 * @param int|null $user_id WordPress user who should receive the redirect. Defaults to the current user.
76 *
77 * @return array{schema_version:int,destination:string,plugin_version:string,source:string,user_id:int} Redirect intent.
78 */
79 public static function create_activation_redirect_intent( $is_initial_install, $plugin_version, $source = 'activation', $user_id = null ) {
80 if ( null === $user_id ) {
81 $user_id = function_exists( 'get_current_user_id' ) ? get_current_user_id() : 0;
82 }
83
84 return array(
85 'schema_version' => self::REDIRECT_SCHEMA_VERSION,
86 'destination' => $is_initial_install ? self::DESTINATION_SETUP_WIZARD : self::DESTINATION_WHATS_NEW,
87 'plugin_version' => sanitize_text_field( (string) $plugin_version ),
88 'source' => sanitize_key( (string) $source ),
89 'user_id' => absint( $user_id ),
90 );
91 }
92
93 /**
94 * Determine whether a transient contains the current first-install intent.
95 *
96 * Boolean values created by earlier Booking Calendar versions deliberately
97 * return false so the released What's New redirect remains their safe fallback.
98 *
99 * @param mixed $redirect_intent Stored activation redirect transient.
100 *
101 * @return bool True for a current, explicit first-install redirect intent.
102 */
103 public static function is_first_install_redirect_intent( $redirect_intent ) {
104 if ( ! is_array( $redirect_intent ) ) {
105 return false;
106 }
107
108 $schema_version = absint( isset( $redirect_intent['schema_version'] ) ? $redirect_intent['schema_version'] : 0 );
109 $destination = isset( $redirect_intent['destination'] ) ? $redirect_intent['destination'] : '';
110
111 return (
112 self::REDIRECT_SCHEMA_VERSION === $schema_version
113 && self::DESTINATION_SETUP_WIZARD === $destination
114 ) || (
115 1 === $schema_version
116 && self::DESTINATION_BOOKING_LISTING === $destination
117 );
118 }
119
120 /**
121 * Determine whether a transient requests the current What's New page.
122 *
123 * Version 1 payloads are accepted so a request that crosses a package update
124 * remains compatible with the earlier 11.9 development contract.
125 *
126 * @param mixed $redirect_intent Stored activation redirect transient.
127 *
128 * @return bool True for a supported What's New redirect intent.
129 */
130 public static function is_whats_new_redirect_intent( $redirect_intent ) {
131 if ( ! is_array( $redirect_intent ) ) {
132 return false;
133 }
134
135 $schema_version = absint( isset( $redirect_intent['schema_version'] ) ? $redirect_intent['schema_version'] : 0 );
136 $destination = isset( $redirect_intent['destination'] ) ? $redirect_intent['destination'] : '';
137
138 return in_array( $schema_version, array( 1, self::REDIRECT_SCHEMA_VERSION ), true )
139 && self::DESTINATION_WHATS_NEW === $destination;
140 }
141
142 /**
143 * Check whether a redirect intent belongs to the current administrator.
144 *
145 * Older payloads and command-line payloads do not contain a positive user ID,
146 * so they retain the released site-scoped behavior.
147 *
148 * @param mixed $redirect_intent Stored activation redirect transient.
149 *
150 * @return bool True when the current request may consume the intent.
151 */
152 public static function is_redirect_intent_for_current_user( $redirect_intent ) {
153 if ( ! is_array( $redirect_intent ) ) {
154 return false;
155 }
156
157 $intent_user_id = absint( isset( $redirect_intent['user_id'] ) ? $redirect_intent['user_id'] : 0 );
158 $current_user_id = function_exists( 'get_current_user_id' ) ? get_current_user_id() : 0;
159
160 return 0 === $intent_user_id || $intent_user_id === absint( $current_user_id );
161 }
162
163 /**
164 * Persist the completed updater request context for the newly installed package.
165 *
166 * WordPress continues executing the previously loaded PHP after replacing a
167 * plugin. The installed file header is therefore used as the target version,
168 * allowing the next request to run the new activation code and decide whether
169 * navigation is appropriate without confusing an automatic update with a
170 * manual Update now action.
171 *
172 * @param string $plugin_file Absolute path to the plugin's main file.
173 * @param string $loaded_version Version of the PHP code loaded for the updater request.
174 *
175 * @return bool True when a newer package context was stored.
176 */
177 public static function record_completed_plugin_update( $plugin_file, $loaded_version ) {
178 $plugin_file = (string) $plugin_file;
179 $loaded_version = sanitize_text_field( (string) $loaded_version );
180 $installed_header = is_readable( $plugin_file ) && function_exists( 'get_file_data' )
181 ? get_file_data( $plugin_file, array( 'version' => 'Version' ), 'plugin' )
182 : array();
183 $installed_version = isset( $installed_header['version'] )
184 ? sanitize_text_field( (string) $installed_header['version'] )
185 : '';
186
187 if ( '' === $installed_version || '' === $loaded_version || version_compare( $installed_version, $loaded_version, '<=' ) ) {
188 return false;
189 }
190
191 $contexts = get_bk_option( self::UPDATE_CONTEXT_OPTION );
192 $contexts = is_array( $contexts ) ? $contexts : array();
193 $context_key = self::get_update_context_key( $plugin_file );
194 $contexts[ $context_key ] = array(
195 'schema_version' => self::UPDATE_CONTEXT_SCHEMA_VERSION,
196 'target_version' => $installed_version,
197 'source' => self::is_manual_single_plugin_update_request( $plugin_file )
198 ? self::UPDATE_SOURCE_MANUAL
199 : self::UPDATE_SOURCE_SUPPRESSED,
200 'user_id' => function_exists( 'get_current_user_id' ) ? absint( get_current_user_id() ) : 0,
201 'recorded_at' => time(),
202 );
203
204 update_bk_option( self::UPDATE_CONTEXT_OPTION, $contexts );
205
206 return true;
207 }
208
209 /**
210 * Read a valid updater context for one package and target version.
211 *
212 * @param string $plugin_file Absolute path to the plugin's main file.
213 * @param string $plugin_version Version now loaded from the installed package.
214 *
215 * @return array|false Valid context, or false for an external/stale replacement.
216 */
217 public static function get_plugin_update_context( $plugin_file, $plugin_version ) {
218 $contexts = get_bk_option( self::UPDATE_CONTEXT_OPTION );
219 $context_key = self::get_update_context_key( $plugin_file );
220 $context = is_array( $contexts ) && isset( $contexts[ $context_key ] ) && is_array( $contexts[ $context_key ] )
221 ? $contexts[ $context_key ]
222 : array();
223 $recorded_at = absint( isset( $context['recorded_at'] ) ? $context['recorded_at'] : 0 );
224 $current_time = time();
225
226 if (
227 self::UPDATE_CONTEXT_SCHEMA_VERSION !== absint( isset( $context['schema_version'] ) ? $context['schema_version'] : 0 )
228 || sanitize_text_field( (string) $plugin_version ) !== ( isset( $context['target_version'] ) ? $context['target_version'] : '' )
229 || 0 === $recorded_at
230 || $recorded_at > ( $current_time + 300 )
231 || $recorded_at < ( $current_time - self::UPDATE_CONTEXT_TTL )
232 || ! in_array(
233 isset( $context['source'] ) ? $context['source'] : '',
234 array( self::UPDATE_SOURCE_MANUAL, self::UPDATE_SOURCE_SUPPRESSED ),
235 true
236 )
237 ) {
238 return false;
239 }
240
241 return $context;
242 }
243
244 /**
245 * Remove one package's updater handoff after activation has completed.
246 *
247 * @param string $plugin_file Absolute path to the plugin's main file.
248 *
249 * @return void
250 */
251 public static function clear_plugin_update_context( $plugin_file ) {
252 $contexts = get_bk_option( self::UPDATE_CONTEXT_OPTION );
253 if ( ! is_array( $contexts ) ) {
254 return;
255 }
256
257 $context_key = self::get_update_context_key( $plugin_file );
258 unset( $contexts[ $context_key ] );
259
260 if ( empty( $contexts ) ) {
261 delete_bk_option( self::UPDATE_CONTEXT_OPTION );
262 return;
263 }
264
265 update_bk_option( self::UPDATE_CONTEXT_OPTION, $contexts );
266 }
267
268 /**
269 * Detect WordPress's interactive, single-plugin Update now request shapes.
270 *
271 * AJAX `update-plugin` and the legacy `update.php?action=upgrade-plugin`
272 * request are intentionally accepted. Cron, WP-CLI, and Network Admin updater
273 * requests are excluded so they cannot take over later navigation. WordPress
274 * sends queued Plugins-screen bulk updates through the same `update-plugin`
275 * request shape, so those remain interactive administrator updates.
276 *
277 * @param string $plugin_file Absolute path to the plugin's main file.
278 *
279 * @return bool True only for an interactive update of this exact plugin.
280 */
281 public static function is_manual_single_plugin_update_request( $plugin_file ) {
282 if (
283 ( defined( 'WP_CLI' ) && WP_CLI )
284 || ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() )
285 || is_network_admin()
286 ) {
287 return false;
288 }
289
290 $expected_plugin = function_exists( 'plugin_basename' ) ? plugin_basename( $plugin_file ) : basename( $plugin_file );
291
292 if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
293 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Core verifies the updater nonce before this post-install filter runs.
294 $request_action = isset( $_POST['action'] ) && is_scalar( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : '';
295 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only request classification after the authorized core update.
296 $request_plugin = isset( $_POST['plugin'] ) && is_scalar( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : '';
297
298 return 'update-plugin' === $request_action
299 && $expected_plugin === ( function_exists( 'plugin_basename' ) ? plugin_basename( $request_plugin ) : $request_plugin );
300 }
301
302 global $pagenow;
303 if ( 'update.php' !== $pagenow ) {
304 return false;
305 }
306
307 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Core verifies the updater nonce before this post-install filter runs.
308 $request_action = isset( $_GET['action'] ) && is_scalar( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
309 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only request classification after the authorized core update.
310 $request_plugin = isset( $_GET['plugin'] ) && is_scalar( $_GET['plugin'] ) ? sanitize_text_field( wp_unslash( $_GET['plugin'] ) ) : '';
311
312 return 'upgrade-plugin' === $request_action
313 && $expected_plugin === ( function_exists( 'plugin_basename' ) ? plugin_basename( $request_plugin ) : $request_plugin );
314 }
315
316 /**
317 * Build an option key that keeps Free and paid package handoffs independent.
318 *
319 * @param string $plugin_file Absolute path to the plugin's main file.
320 *
321 * @return string Stable non-sensitive package key.
322 */
323 private static function get_update_context_key( $plugin_file ) {
324 $plugin_basename = function_exists( 'plugin_basename' ) ? plugin_basename( $plugin_file ) : basename( $plugin_file );
325
326 return md5( strtolower( (string) $plugin_basename ) );
327 }
328
329 /**
330 * Check environments where automatic onboarding must never take over navigation.
331 *
332 * Manual access to Setup Wizard remains governed by its normal access and
333 * environment policies. This restriction applies only to automatic activation
334 * redirects and the one-time Booking Listing invitation.
335 *
336 * @return bool True when automatic onboarding must remain suppressed.
337 */
338 public static function is_automatic_onboarding_restricted() {
339 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only activation context detection.
340 $is_bulk_activation = isset( $_GET['activate-multi'] );
341
342 return ! WPBC_Environment_Policy::allows_automatic_setup_onboarding()
343 || is_network_admin()
344 || $is_bulk_activation
345 || defined( 'IFRAME_REQUEST' );
346 }
347 }
348