PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / Activator.php

Activator.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at includes/Activator.php

718 lines 24.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Activation checks and set up.
4 *
5 * @author Paul Kilmurray <paul@kilbot.com>
6 *
7 * @see http://wcpos.com
8 * @package WCPOS\WooCommercePOS
9 */
10
11 namespace WCPOS\WooCommercePOS;
12
13 use WCPOS\WooCommercePOS\Admin\Consent;
14 use WCPOS\WooCommercePOS\Services\Lifecycle_Events;
15 use WCPOS\WooCommercePOS\Sync\Api as Sync_Api;
16 use WCPOS\WooCommercePOS\Sync\Health as Sync_Health;
17 use WCPOS\WooCommercePOS\Sync\Integrity_Digest;
18 use WCPOS\WooCommercePOS\Sync\Mutation_Store;
19 use WCPOS\WooCommercePOS\Sync\Sync_Journal;
20 use const DOING_AJAX;
21
22 /**
23 * Activator class.
24 */
25 class Activator {
26 /**
27 * Lock name used by WP_Upgrader::create_lock().
28 */
29 private const DB_UPGRADE_LOCK_NAME = 'woocommerce_pos_db_upgrade_lock';
30
31 /**
32 * Lock TTL in seconds.
33 */
34 private const DB_UPGRADE_LOCK_TTL = 600;
35
36 /**
37 * Constructor.
38 */
39 public function __construct() {
40 register_activation_hook( PLUGIN_FILE, array( $this, 'activate' ) );
41 add_action( 'wpmu_new_blog', array( $this, 'activate_new_site' ) );
42 add_action( 'plugins_loaded', array( $this, 'init' ) );
43 }
44
45 /**
46 * Checks for valid install and begins execution of the plugin.
47 */
48 public function init(): void {
49 // Check for min requirements to run.
50 if ( $this->php_check() && $this->woocommerce_check() ) {
51 // Defer permalink check to admin_init so __() calls happen after
52 // after_setup_theme (WordPress 6.7+ triggers a notice otherwise).
53 if ( is_admin() && ( ! \defined( '\DOING_AJAX' ) || ! DOING_AJAX ) ) { // @phpstan-ignore-line
54 add_action(
55 'admin_init',
56 function () {
57 $this->permalink_check();
58 }
59 );
60 }
61
62 // Init update script if required.
63 $this->version_check();
64 $this->pro_version_check();
65
66 // resolve plugin plugins.
67 $this->plugin_check();
68
69 new Init();
70 }
71 }
72
73 /**
74 * Fired when the plugin is activated.
75 *
76 * @param bool $network_wide Whether to activate network-wide.
77 */
78 public function activate( $network_wide ): void {
79 if ( \function_exists( 'is_multisite' ) && is_multisite() ) {
80 if ( $network_wide ) {
81 // Get all blog ids.
82 $blog_ids = $this->get_blog_ids();
83
84 foreach ( $blog_ids as $blog_id ) {
85 switch_to_blog( $blog_id );
86 $this->single_activate();
87
88 restore_current_blog();
89 }
90 } else {
91 self::single_activate();
92 }
93 } else {
94 self::single_activate();
95 }
96 }
97
98 /**
99 * Fired when the plugin is activated.
100 *
101 * @param bool $install_sync_schema Whether to install the sync schema.
102 */
103 public function single_activate( bool $install_sync_schema = true ): void {
104 $role_capabilities = self::role_capability_definition();
105
106 // Reseed the default template terms on the next request: (re)activation
107 // is the repair a merchant reaches for after deleting a term by hand.
108 // This also runs once per upgrade (version_check re-activates to sync
109 // role caps), so one post-upgrade request pays the ~18 seeding queries.
110 delete_option( Templates::DEFAULT_TERMS_OPTION );
111
112 // Second, merchant-reachable trigger for the autoload repair: db_upgrade()
113 // only runs when version_check() trips on an admin load that reaches
114 // woocommerce_init, and a miss there is permanent once bump_versions() ran.
115 self::autoload_request_latches();
116 Admin\Permalink::ensure_default();
117
118 // create POS specific roles.
119 $this->create_pos_roles();
120
121 // add pos capabilities to non POS roles.
122 $this->add_pos_capability(
123 array(
124 'administrator' => $role_capabilities['administrator'],
125 'shop_manager' => $role_capabilities['shop_manager'],
126 )
127 );
128
129 $stored_roles = get_option( wp_roles()->role_key, array() );
130 $roles_are_persisted = is_array( $stored_roles );
131 if ( $roles_are_persisted ) {
132 foreach ( $role_capabilities as $slug => $capabilities ) {
133 $required_capabilities = 'cashier' === $slug
134 ? array_merge( array( 'access_woocommerce_pos' ), array_keys( $capabilities ) )
135 : $capabilities;
136 foreach ( $required_capabilities as $capability ) {
137 if ( empty( $stored_roles[ $slug ]['capabilities'][ $capability ] ) ) {
138 $roles_are_persisted = false;
139 break 2;
140 }
141 }
142 }
143 }
144
145 $obsolete_customer_create_cap = isset( $role_capabilities['cashier']['create_customers'] ) ? 'promote_users' : 'create_customers';
146 if ( $roles_are_persisted && empty( $stored_roles['cashier']['capabilities'][ $obsolete_customer_create_cap ] ) ) {
147 update_option( 'woocommerce_pos_role_caps_fingerprint', $this->role_caps_fingerprint(), true );
148 }
149
150 // Flag the consent pop-up for the next admin page load. Done here
151 // because the `activated_plugin` action in Admin\Consent fires
152 // inside the activation request, at which point our plugin's
153 // `plugins_loaded` callback hasn't yet instantiated Init on a
154 // fresh install.
155 //
156 // Read the option directly — woocommerce_pos_get_settings() lives in
157 // wcpos-functions.php which Init loads on `plugins_loaded`, but
158 // plugins_loaded has already fired by the time activation runs.
159 $general_settings = get_option( 'woocommerce_pos_settings_general', array() );
160 $tracking_consent = is_array( $general_settings ) && isset( $general_settings['tracking_consent'] )
161 ? $general_settings['tracking_consent']
162 : 'undecided';
163 if ( 'undecided' === $tracking_consent ) {
164 set_transient( Consent::MODAL_TRANSIENT, 1, Consent::MODAL_TRANSIENT_TTL );
165 }
166
167 if ( $install_sync_schema ) {
168 $this->install_sync_schema();
169 }
170
171 // Record the install for analytics. Consent is still `undecided` at this
172 // point, so the event is held until the user answers the pop-up flagged
173 // above; Lifecycle_Events owns that deferral and reports at most once.
174 ( new Lifecycle_Events() )->record_install();
175 }
176
177 /**
178 * Install the sync store and latch its aggregate schema version after verification.
179 */
180 public function install_sync_schema(): void {
181 $previous_schema = get_option( Sync_Api::SCHEMA_OPTION, null );
182
183 $journal = new Sync_Journal();
184 $journal->install();
185 ( new Integrity_Digest() )->install();
186 ( new Mutation_Store() )->install();
187
188 if ( ! Sync_Health::is_healthy() ) {
189 if ( Sync_Api::SCHEMA_VERSION === $previous_schema ) {
190 delete_option( Sync_Api::SCHEMA_OPTION );
191 }
192 return;
193 }
194
195 // Schema 3 (#1379): the customer space widened from role=customer to ALL users.
196 // Upgrading installs carry role-departure tombstones in the persisted stream that
197 // would replay against now-live users; compensating updates supersede them (see
198 // Sync_Journal::append_customer_updates_for_all_users). The old latch stays until
199 // migration succeeds, so retries may append duplicate but harmless superseding
200 // updates. Fresh installs (no previous latch) have no stream to repair.
201 if (
202 null !== $previous_schema
203 && version_compare( (string) $previous_schema, '3', '<' )
204 && ! $journal->append_customer_updates_for_all_users()
205 ) {
206 return;
207 }
208
209 // Autoloaded: the Init constructor reads this latch on every request.
210 // This flips an existing row only on WP 6.4+; older rows are flipped by
211 // autoload_request_latches() on upgrade.
212 update_option( Sync_Api::SCHEMA_OPTION, Sync_Api::SCHEMA_VERSION, true );
213
214 if ( null !== $previous_schema && version_compare( (string) $previous_schema, Sync_Api::SCHEMA_VERSION, '<' ) ) {
215 global $wpdb;
216 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wcpos_sync_change_log" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Known legacy table name.
217 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wcpos_sync_order_index" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Known legacy table name.
218 // The legacy Change_Log_Purge class is gone; its recurring cron event
219 // would otherwise survive the upgrade and fire a hook with no handler
220 // forever. Literal hook name — the constant was removed with the class.
221 wp_clear_scheduled_hook( 'wcpos_change_log_purge' );
222 }
223 }
224
225 /**
226 * Fired when a new site is activated with a WPMU environment.
227 *
228 * @param int $blog_id Blog ID.
229 */
230 public function activate_new_site( $blog_id ): void {
231 if ( 1 !== did_action( 'wpmu_new_blog' ) ) {
232 return;
233 }
234
235 switch_to_blog( $blog_id );
236 $this->single_activate();
237 restore_current_blog();
238 }
239
240 /**
241 * Check min version of PHP.
242 */
243 private function php_check() {
244 $php_version = PHP_VERSION;
245 if ( version_compare( $php_version, PHP_MIN_VERSION, '>' ) ) {
246 return true;
247 }
248
249 // Defer __() call to avoid "too early" warning in WordPress 6.7+.
250 add_action(
251 'admin_init',
252 function () {
253 $message = \sprintf(
254 // translators: 1: Minimum PHP version, 2: Update URL.
255 __( '<strong>WCPOS</strong> requires PHP %1$s or higher. Read more information about <a href="%2$s">how you can update</a>', 'woocommerce-pos' ),
256 PHP_MIN_VERSION,
257 'http://www.wpupdatephp.com/update/'
258 ) . ' &raquo;';
259
260 Admin\Notices::add( $message );
261 }
262 );
263 }
264
265 /**
266 * Check min version of WooCommerce installed.
267 */
268 private function woocommerce_check() {
269 if ( class_exists( '\WooCommerce' ) && version_compare( WC()->version, WC_MIN_VERSION, '>=' ) ) {
270 return true;
271 }
272
273 // Defer __() call to avoid "too early" warning in WordPress 6.7+.
274 add_action(
275 'admin_init',
276 function () {
277 $message = \sprintf(
278 // translators: 1: WooCommerce URL, 2: Minimum WC version, 3: Plugins URL.
279 __( '<strong>WCPOS</strong> requires <a href="%1$s">WooCommerce %2$s or higher</a>. Please <a href="%3$s">install and activate WooCommerce</a>', 'woocommerce-pos' ),
280 'http://wordpress.org/plugins/woocommerce/',
281 WC_MIN_VERSION,
282 admin_url( 'plugins.php' )
283 ) . ' &raquo;';
284
285 Admin\Notices::add( $message );
286 }
287 );
288 }
289
290 /**
291 * POS Frontend will give 404 if pretty permalinks not active.
292 */
293 private function permalink_check(): void {
294 $permalinks = get_option( 'permalink_structure' );
295
296 // early return.
297 if ( $permalinks ) {
298 return;
299 }
300
301 $message = /* translators: Plugin activation notice label. */ __( '<strong>WooCommerce REST API</strong> requires <em>pretty</em> permalinks to work correctly', 'woocommerce-pos' ) . '. ';
302 $message .= \sprintf( '<a href="%s">%s</a>', admin_url( 'options-permalink.php' ), /* translators: Plugin activation notice label. */ __( 'Enable permalinks', 'woocommerce-pos' ) ) . ' &raquo;';
303
304 Admin\Notices::add( $message );
305 }
306
307 /**
308 * Check version number, runs every admin page load.
309 */
310 private function version_check(): void {
311 $old = (string) Services\Settings::get_db_version();
312 $plugin_needs_upgrade = version_compare( $old, VERSION, '<' );
313 $sync_needs_upgrade = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null );
314
315 $role_caps_fingerprint = $this->role_caps_fingerprint();
316 $role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $role_caps_fingerprint;
317 if ( ! $plugin_needs_upgrade && ! $sync_needs_upgrade && ! $role_caps_need_sync ) {
318 return;
319 }
320
321 if ( ! $this->acquire_db_upgrade_lock() ) {
322 return;
323 }
324
325 $locked_old = (string) Services\Settings::get_db_version();
326 $locked_plugin_needs_upgrade = version_compare( $locked_old, VERSION, '<' );
327 $locked_sync_needs_upgrade = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null );
328
329 $locked_role_caps_fingerprint = $this->role_caps_fingerprint();
330 $locked_role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $locked_role_caps_fingerprint;
331 if ( ! $locked_plugin_needs_upgrade && ! $locked_sync_needs_upgrade && ! $locked_role_caps_need_sync ) {
332 $this->release_db_upgrade_lock();
333 return;
334 }
335
336 if ( $locked_plugin_needs_upgrade ) {
337 Services\Settings::bump_versions();
338 }
339
340 if ( $locked_plugin_needs_upgrade || $locked_role_caps_need_sync ) {
341 // Re-run activation to sync role capabilities. add_role() and add_cap()
342 // are both idempotent, so this is safe. Without this, capabilities added
343 // in newer versions would never reach existing installs because add_role()
344 // is a no-op when the role already exists.
345 // Deferred to 'init' because create_pos_roles() calls __() which
346 // requires translations to be loaded (WordPress 6.7+).
347 add_action(
348 'init',
349 function () {
350 $this->single_activate( false );
351 }
352 );
353 }
354
355 $lock_released = false;
356 $release_lock = function () use ( &$lock_released ): void {
357 if ( $lock_released ) {
358 return;
359 }
360
361 $lock_released = true;
362 $this->release_db_upgrade_lock();
363 };
364
365 // Safety net in case woocommerce_init does not fire for this request.
366 add_action( 'shutdown', $release_lock );
367
368 // Defer db_upgrade to woocommerce_init when WC is fully loaded.
369 // This prevents conflicts with plugins like WC Subscriptions that hook
370 // into before_delete_post and assume WC()->order_factory is available.
371 add_action(
372 'woocommerce_init',
373 function () use ( $locked_old, $locked_plugin_needs_upgrade, $locked_sync_needs_upgrade, $release_lock ) {
374 try {
375 $this->db_upgrade( $locked_old, VERSION );
376
377 // Report the upgrade only once the migration has actually
378 // completed — queueing it beside bump_versions() would claim a
379 // finished upgrade even when db_upgrade() threw or never ran.
380 // Still exactly-once: the version was bumped above, so the
381 // upgrade is not re-detected on the next request.
382 if ( $locked_plugin_needs_upgrade ) {
383 ( new Lifecycle_Events() )->record_upgrade( $locked_old, VERSION );
384 }
385 if ( $locked_sync_needs_upgrade && Sync_Api::SCHEMA_VERSION === get_option( Sync_Api::SCHEMA_OPTION, null ) ) {
386 ( new Sync_Journal() )->register_hooks();
387 ( new Integrity_Digest() )->register_hooks();
388 }
389 } finally {
390 $release_lock();
391 remove_action( 'shutdown', $release_lock );
392 }
393 }
394 );
395 }
396
397 /**
398 * Acquire the DB upgrade lock.
399 *
400 * @return bool True when this request owns the lock.
401 */
402 private function acquire_db_upgrade_lock(): bool {
403 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
404
405 return \WP_Upgrader::create_lock( self::DB_UPGRADE_LOCK_NAME, self::DB_UPGRADE_LOCK_TTL );
406 }
407
408 /**
409 * Release the DB upgrade lock.
410 */
411 private function release_db_upgrade_lock(): void {
412 if ( ! class_exists( '\WP_Upgrader', false ) ) {
413 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
414 }
415
416 \WP_Upgrader::release_lock( self::DB_UPGRADE_LOCK_NAME );
417 }
418
419 /**
420 * Plugin conflicts.
421 *
422 * - NextGEN Gallery is a terrible plugin. It buffers all content on 'init' action, priority -1 and inserts junk code.
423 */
424 private function plugin_check(): void {
425 // disable NextGEN Gallery resource manager
426 // if ( ! \defined( 'NGG_DISABLE_RESOURCE_MANAGER' ) ) {
427 // \define( 'NGG_DISABLE_RESOURCE_MANAGER', true );
428 // }.
429 }
430
431 /**
432 * Get all blog ids of blogs in the current network that are:
433 * - not archived
434 * - not spam
435 * - not deleted.
436 */
437 private function get_blog_ids() {
438 global $wpdb;
439
440 // get an array of blog ids.
441 $sql = "SELECT blog_id FROM $wpdb->blogs
442 WHERE archived = '0' AND spam = '0'
443 AND deleted = '0'";
444
445 return $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query, no user input
446 }
447
448 /**
449 * Get the role capability definition.
450 *
451 * @return array<string, array<string, bool>|array<int, string>> Role capabilities keyed by role.
452 */
453 private static function role_capability_definition(): array {
454 // WC 9.9 replaced promote_users with create_customers for customer creation.
455 $customer_create_cap = \defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '9.9', '>=' ) // @phpstan-ignore-line
456 ? 'create_customers'
457 : 'promote_users';
458
459 // Cashier role.
460 $cashier_capabilities = array(
461 'read' => true,
462 'read_private_products' => true,
463 'publish_products' => true,
464 'edit_product' => true,
465 'edit_products' => true,
466 'edit_published_products' => true,
467 'edit_private_products' => true,
468 'edit_others_products' => true,
469 'read_private_shop_orders' => true,
470 'publish_shop_orders' => true,
471 'edit_shop_orders' => true,
472 'edit_others_shop_orders' => true,
473 'list_users' => true,
474 $customer_create_cap => true,
475 'edit_users' => true,
476 'read_private_shop_coupons' => true,
477 'publish_shop_coupons' => true,
478 'edit_shop_coupons' => true,
479 'edit_published_shop_coupons' => true,
480 'edit_private_shop_coupons' => true,
481 'edit_others_shop_coupons' => true,
482 'manage_product_terms' => true,
483 );
484
485 return array(
486 'cashier' => $cashier_capabilities,
487 'administrator' => array(
488 'manage_woocommerce_pos',
489 'access_woocommerce_pos',
490 'edit_wcpos_store',
491 'read_wcpos_store',
492 'delete_wcpos_store',
493 'edit_wcpos_stores',
494 'edit_others_wcpos_stores',
495 'publish_wcpos_stores',
496 'read_private_wcpos_stores',
497 'delete_wcpos_stores',
498 'delete_private_wcpos_stores',
499 'delete_published_wcpos_stores',
500 'delete_others_wcpos_stores',
501 'edit_private_wcpos_stores',
502 'edit_published_wcpos_stores',
503 ),
504 'shop_manager' => array( 'manage_woocommerce_pos', 'access_woocommerce_pos' ),
505 );
506 }
507
508 /**
509 * Get the role-capabilities definition fingerprint.
510 */
511 private function role_caps_fingerprint(): string {
512 return md5( wp_json_encode( self::role_capability_definition() ) );
513 }
514
515 /**
516 * Add POS specific roles.
517 */
518 private function create_pos_roles(): void {
519 $role_capabilities = self::role_capability_definition();
520 $cashier_capabilities = $role_capabilities['cashier'];
521
522 add_role(
523 'cashier',
524 /* translators: Plugin activation notice label. */
525 __( 'Cashier', 'woocommerce-pos' ),
526 $cashier_capabilities
527 );
528
529 $obsolete_customer_create_cap = isset( $cashier_capabilities['create_customers'] ) ? 'promote_users' : 'create_customers';
530 $cashier = get_role( 'cashier' );
531 if ( $cashier ) {
532 $cashier->remove_cap( $obsolete_customer_create_cap );
533 }
534
535 // Sync all capabilities to the existing role. add_role() is a no-op when
536 // the role already exists, so capabilities added in newer versions would
537 // never reach existing installs without this.
538 $this->add_pos_capability(
539 array(
540 'cashier' => array_merge(
541 array( 'access_woocommerce_pos' ),
542 array_keys( $cashier_capabilities )
543 ),
544 )
545 );
546 }
547
548 /**
549 * Add default pos capabilities to administrator and shop_manager roles.
550 *
551 * @param array $roles An array of arrays representing the roles and their POS capabilities.
552 */
553 private function add_pos_capability( $roles ): void {
554 foreach ( $roles as $slug => $caps ) {
555 $role = get_role( $slug );
556 if ( $role ) {
557 foreach ( $caps as $cap ) {
558 $role->add_cap( $cap );
559 }
560 }
561 }
562 }
563
564 /**
565 * Upgrade database.
566 *
567 * @param string $old Old version.
568 * @param string $current Current version.
569 */
570 private function db_upgrade( $old, $current ): void {
571 $db_updates = array(
572 '0.4' => 'updates/update-0.4.php',
573 '0.4.6' => 'updates/update-0.4.6.php',
574 '1.0.0-beta.1' => 'updates/update-1.0.0-beta.1.php',
575 '1.6.1' => 'updates/update-1.6.1.php',
576 '1.8.0' => 'updates/update-1.8.0.php',
577 '1.8.7' => 'updates/update-1.8.7.php',
578 '1.8.12' => 'updates/update-1.8.12.php',
579 '1.8.13' => 'updates/update-1.8.13.php',
580 '1.9.0' => 'updates/update-1.9.0.php',
581 '1.10.0' => 'updates/update-1.10.0.php',
582 );
583 foreach ( $db_updates as $version => $updater ) {
584 if ( version_compare( $version, $old, '>' ) &&
585 version_compare( $version, $current, '<=' ) ) {
586 include $updater;
587 }
588 }
589
590 if ( Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null ) ) {
591 $this->install_sync_schema();
592 }
593
594 // Installs that predate 2026-09 wrote the per-request latches with
595 // autoload off; every upgrade re-asserts autoload so the flip is
596 // idempotent and needs no versioned update file.
597 self::autoload_request_latches();
598 Admin\Permalink::ensure_default();
599 }
600
601 /**
602 * Every option row that is read on EVERY request and must therefore ride in
603 * alloptions: the three sync latches the Init constructor reads, the permalink
604 * slug Template_Router reads, and each registered settings section that
605 * declares {@see Services\Settings\Abstract_Section::autoload()} — the
606 * sections are the extension point, so Pro's and extensions' sections join
607 * the repair by declaring it, without touching this file.
608 *
609 * Needed because core's update_option() returns early on an unchanged value
610 * WITHOUT touching the autoload column, so a writer alone never repairs a row
611 * an older release wrote with autoload off. Without a persistent object cache
612 * each such row cost one `SELECT option_value` per page load (measured
613 * 2026-09-03 on dev-next and dev-free).
614 *
615 * @return string[]
616 */
617 private static function request_option_names(): array {
618 $names = array(
619 Sync_Api::SCHEMA_OPTION,
620 \WCPOS\WooCommercePOS\Sync\Visibility_Observer::SEED_VERSION_OPTION,
621 \WCPOS\WooCommercePOS\Sync\Config_Fingerprint::CLEANUP_VERSION_OPTION,
622 Admin\Permalink::DB_KEY,
623 );
624 foreach ( Services\Settings::instance()->sections()->all() as $section ) {
625 if ( $section instanceof Services\Settings\Abstract_Section && $section->autoload() ) {
626 $names[] = $section->autoload_option_name();
627 }
628 }
629 return $names;
630 }
631
632 /**
633 * Flip the per-request rows to autoload in place, and seed the settings
634 * sections that are absent.
635 *
636 * One UPDATE on the flag column: never delete-and-recreate, because
637 * `Sync_Api::SCHEMA_OPTION` gates the sync observers on every request and a
638 * request landing in that gap would run with journaling off. 'yes' is
639 * accepted by every core version (6.6+ maps it alongside 'on'). Idempotent:
640 * already-autoloaded rows match nothing. Latches that were never written
641 * stay absent (their absence is the signal). An autoloaded settings section
642 * that was never saved is seeded as an autoloaded row — an absent option is
643 * queried on every request too. General also persists its migrated consent
644 * so the legacy row does not remain on the read path; other defaults stay
645 * dynamic.
646 */
647 public static function autoload_request_latches(): void {
648 global $wpdb;
649 // The key comes from the section itself (autoload_option_name()): Pro's
650 // License section stores under a Pro-prefixed key, so deriving it from
651 // id() flipped nothing for that row and seeded a stray free-prefixed one.
652 foreach ( Services\Settings::instance()->sections()->all() as $section ) {
653 if ( ! $section instanceof Services\Settings\Abstract_Section || ! $section->autoload() ) {
654 continue;
655 }
656 $option_name = $section->autoload_option_name();
657 if ( false === get_option( $option_name ) ) {
658 $value = $section instanceof Services\Settings\General_Section
659 ? array( 'tracking_consent' => $section->raw_tracking_consent() )
660 : array();
661 add_option( $option_name, $value, '', true );
662 }
663 }
664 $options = self::request_option_names();
665 $placeholders = implode( ', ', array_fill( 0, \count( $options ), '%s' ) );
666 $flipped = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the flag column is the target; caches are cleared below.
667 $wpdb->prepare(
668 "UPDATE {$wpdb->options} SET autoload = 'yes' WHERE option_name IN ({$placeholders}) AND autoload NOT IN ('yes', 'on', 'auto-on')", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholders are generated for the prepared values.
669 $options
670 )
671 );
672 if ( ! $flipped ) {
673 return;
674 }
675 foreach ( $options as $option ) {
676 wp_cache_delete( $option, 'options' );
677 }
678 wp_cache_delete( 'alloptions', 'options' );
679 }
680
681 /**
682 * If \WCPOS\WooCommercePOSPro\ is installed, check the version is above MIN_PRO_VERSION.
683 */
684 private function pro_version_check(): void {
685 if ( class_exists( '\WCPOS\WooCommercePOSPro\Activator' ) ) {
686 if ( version_compare( \WCPOS\WooCommercePOSPro\VERSION, MIN_PRO_VERSION, '<' ) ) { // @phpstan-ignore-line
687
688 /*
689 * NOTE: the deactivate_plugins function is not available in the frontend or ajax
690 * This is an extreme situation where the Pro plugin could crash the site, so we need to deactivate it
691 */
692 if ( ! \function_exists( 'deactivate_plugins' ) ) {
693 require_once ABSPATH . '/wp-admin/includes/plugin.php';
694 }
695
696 // WCPOS Pro is activated, but the version is too low - use the constant for dynamic folder name.
697 deactivate_plugins( \WCPOS\WooCommercePOSPro\PLUGIN_FILE ); // @phpstan-ignore-line
698
699 // Defer __() call to avoid "too early" warning in WordPress 6.7+.
700 add_action(
701 'admin_init',
702 function () {
703 $message = \sprintf(
704 // translators: 1: WCPOS Pro URL, 2: Minimum Pro version, 3: Plugins URL.
705 __( '<strong>WCPOS</strong> requires <a href="%1$s">WCPOS Pro %2$s or higher</a>. Please <a href="%3$s">install and activate WCPOS Pro</a>', 'woocommerce-pos' ),
706 'https://wcpos.com/my-account',
707 MIN_PRO_VERSION,
708 admin_url( 'plugins.php' )
709 ) . ' &raquo;';
710
711 Admin\Notices::add( $message );
712 }
713 );
714 }
715 }
716 }
717 }
718