PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.6
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.6
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 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 All 163 releases
woocommerce-pos / includes / Activator.php

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

617 lines 20.0 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 // create POS specific roles.
107 $this->create_pos_roles();
108
109 // add pos capabilities to non POS roles.
110 $this->add_pos_capability(
111 array(
112 'administrator' => $role_capabilities['administrator'],
113 'shop_manager' => $role_capabilities['shop_manager'],
114 )
115 );
116
117 $stored_roles = get_option( wp_roles()->role_key, array() );
118 $roles_are_persisted = is_array( $stored_roles );
119 if ( $roles_are_persisted ) {
120 foreach ( $role_capabilities as $slug => $capabilities ) {
121 $required_capabilities = 'cashier' === $slug
122 ? array_merge( array( 'access_woocommerce_pos' ), array_keys( $capabilities ) )
123 : $capabilities;
124 foreach ( $required_capabilities as $capability ) {
125 if ( empty( $stored_roles[ $slug ]['capabilities'][ $capability ] ) ) {
126 $roles_are_persisted = false;
127 break 2;
128 }
129 }
130 }
131 }
132
133 $obsolete_customer_create_cap = isset( $role_capabilities['cashier']['create_customers'] ) ? 'promote_users' : 'create_customers';
134 if ( $roles_are_persisted && empty( $stored_roles['cashier']['capabilities'][ $obsolete_customer_create_cap ] ) ) {
135 update_option( 'woocommerce_pos_role_caps_fingerprint', $this->role_caps_fingerprint(), true );
136 }
137
138 // Flag the consent pop-up for the next admin page load. Done here
139 // because the `activated_plugin` action in Admin\Consent fires
140 // inside the activation request, at which point our plugin's
141 // `plugins_loaded` callback hasn't yet instantiated Init on a
142 // fresh install.
143 //
144 // Read the option directly — woocommerce_pos_get_settings() lives in
145 // wcpos-functions.php which Init loads on `plugins_loaded`, but
146 // plugins_loaded has already fired by the time activation runs.
147 $general_settings = get_option( 'woocommerce_pos_settings_general', array() );
148 $tracking_consent = is_array( $general_settings ) && isset( $general_settings['tracking_consent'] )
149 ? $general_settings['tracking_consent']
150 : 'undecided';
151 if ( 'undecided' === $tracking_consent ) {
152 set_transient( Consent::MODAL_TRANSIENT, 1, Consent::MODAL_TRANSIENT_TTL );
153 }
154
155 if ( $install_sync_schema ) {
156 $this->install_sync_schema();
157 }
158
159 // Record the install for analytics. Consent is still `undecided` at this
160 // point, so the event is held until the user answers the pop-up flagged
161 // above; Lifecycle_Events owns that deferral and reports at most once.
162 ( new Lifecycle_Events() )->record_install();
163 }
164
165 /**
166 * Install the sync store and latch its aggregate schema version after verification.
167 */
168 public function install_sync_schema(): void {
169 $previous_schema = get_option( Sync_Api::SCHEMA_OPTION, null );
170
171 $journal = new Sync_Journal();
172 $journal->install();
173 ( new Integrity_Digest() )->install();
174 ( new Mutation_Store() )->install();
175
176 if ( ! Sync_Health::is_healthy() ) {
177 if ( Sync_Api::SCHEMA_VERSION === $previous_schema ) {
178 delete_option( Sync_Api::SCHEMA_OPTION );
179 }
180 return;
181 }
182
183 // Schema 3 (#1379): the customer space widened from role=customer to ALL users.
184 // Upgrading installs carry role-departure tombstones in the persisted stream that
185 // would replay against now-live users; compensating updates supersede them (see
186 // Sync_Journal::append_customer_updates_for_all_users). The old latch stays until
187 // migration succeeds, so retries may append duplicate but harmless superseding
188 // updates. Fresh installs (no previous latch) have no stream to repair.
189 if (
190 null !== $previous_schema
191 && version_compare( (string) $previous_schema, '3', '<' )
192 && ! $journal->append_customer_updates_for_all_users()
193 ) {
194 return;
195 }
196
197 update_option( Sync_Api::SCHEMA_OPTION, Sync_Api::SCHEMA_VERSION, false );
198
199 if ( null !== $previous_schema && version_compare( (string) $previous_schema, Sync_Api::SCHEMA_VERSION, '<' ) ) {
200 global $wpdb;
201 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wcpos_sync_change_log" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Known legacy table name.
202 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wcpos_sync_order_index" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Known legacy table name.
203 // The legacy Change_Log_Purge class is gone; its recurring cron event
204 // would otherwise survive the upgrade and fire a hook with no handler
205 // forever. Literal hook name — the constant was removed with the class.
206 wp_clear_scheduled_hook( 'wcpos_change_log_purge' );
207 }
208 }
209
210 /**
211 * Fired when a new site is activated with a WPMU environment.
212 *
213 * @param int $blog_id Blog ID.
214 */
215 public function activate_new_site( $blog_id ): void {
216 if ( 1 !== did_action( 'wpmu_new_blog' ) ) {
217 return;
218 }
219
220 switch_to_blog( $blog_id );
221 $this->single_activate();
222 restore_current_blog();
223 }
224
225 /**
226 * Check min version of PHP.
227 */
228 private function php_check() {
229 $php_version = PHP_VERSION;
230 if ( version_compare( $php_version, PHP_MIN_VERSION, '>' ) ) {
231 return true;
232 }
233
234 // Defer __() call to avoid "too early" warning in WordPress 6.7+.
235 add_action(
236 'admin_init',
237 function () {
238 $message = \sprintf(
239 // translators: 1: Minimum PHP version, 2: Update URL.
240 __( '<strong>WCPOS</strong> requires PHP %1$s or higher. Read more information about <a href="%2$s">how you can update</a>', 'woocommerce-pos' ),
241 PHP_MIN_VERSION,
242 'http://www.wpupdatephp.com/update/'
243 ) . ' &raquo;';
244
245 Admin\Notices::add( $message );
246 }
247 );
248 }
249
250 /**
251 * Check min version of WooCommerce installed.
252 */
253 private function woocommerce_check() {
254 if ( class_exists( '\WooCommerce' ) && version_compare( WC()->version, WC_MIN_VERSION, '>=' ) ) {
255 return true;
256 }
257
258 // Defer __() call to avoid "too early" warning in WordPress 6.7+.
259 add_action(
260 'admin_init',
261 function () {
262 $message = \sprintf(
263 // translators: 1: WooCommerce URL, 2: Minimum WC version, 3: Plugins URL.
264 __( '<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' ),
265 'http://wordpress.org/plugins/woocommerce/',
266 WC_MIN_VERSION,
267 admin_url( 'plugins.php' )
268 ) . ' &raquo;';
269
270 Admin\Notices::add( $message );
271 }
272 );
273 }
274
275 /**
276 * POS Frontend will give 404 if pretty permalinks not active.
277 */
278 private function permalink_check(): void {
279 $permalinks = get_option( 'permalink_structure' );
280
281 // early return.
282 if ( $permalinks ) {
283 return;
284 }
285
286 $message = /* translators: Plugin activation notice label. */ __( '<strong>WooCommerce REST API</strong> requires <em>pretty</em> permalinks to work correctly', 'woocommerce-pos' ) . '. ';
287 $message .= \sprintf( '<a href="%s">%s</a>', admin_url( 'options-permalink.php' ), /* translators: Plugin activation notice label. */ __( 'Enable permalinks', 'woocommerce-pos' ) ) . ' &raquo;';
288
289 Admin\Notices::add( $message );
290 }
291
292 /**
293 * Check version number, runs every admin page load.
294 */
295 private function version_check(): void {
296 $old = (string) Services\Settings::get_db_version();
297 $plugin_needs_upgrade = version_compare( $old, VERSION, '<' );
298 $sync_needs_upgrade = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null );
299
300 $role_caps_fingerprint = $this->role_caps_fingerprint();
301 $role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $role_caps_fingerprint;
302 if ( ! $plugin_needs_upgrade && ! $sync_needs_upgrade && ! $role_caps_need_sync ) {
303 return;
304 }
305
306 if ( ! $this->acquire_db_upgrade_lock() ) {
307 return;
308 }
309
310 $locked_old = (string) Services\Settings::get_db_version();
311 $locked_plugin_needs_upgrade = version_compare( $locked_old, VERSION, '<' );
312 $locked_sync_needs_upgrade = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null );
313
314 $locked_role_caps_fingerprint = $this->role_caps_fingerprint();
315 $locked_role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $locked_role_caps_fingerprint;
316 if ( ! $locked_plugin_needs_upgrade && ! $locked_sync_needs_upgrade && ! $locked_role_caps_need_sync ) {
317 $this->release_db_upgrade_lock();
318 return;
319 }
320
321 if ( $locked_plugin_needs_upgrade ) {
322 Services\Settings::bump_versions();
323 }
324
325 if ( $locked_plugin_needs_upgrade || $locked_role_caps_need_sync ) {
326 // Re-run activation to sync role capabilities. add_role() and add_cap()
327 // are both idempotent, so this is safe. Without this, capabilities added
328 // in newer versions would never reach existing installs because add_role()
329 // is a no-op when the role already exists.
330 // Deferred to 'init' because create_pos_roles() calls __() which
331 // requires translations to be loaded (WordPress 6.7+).
332 add_action(
333 'init',
334 function () {
335 $this->single_activate( false );
336 }
337 );
338 }
339
340 $lock_released = false;
341 $release_lock = function () use ( &$lock_released ): void {
342 if ( $lock_released ) {
343 return;
344 }
345
346 $lock_released = true;
347 $this->release_db_upgrade_lock();
348 };
349
350 // Safety net in case woocommerce_init does not fire for this request.
351 add_action( 'shutdown', $release_lock );
352
353 // Defer db_upgrade to woocommerce_init when WC is fully loaded.
354 // This prevents conflicts with plugins like WC Subscriptions that hook
355 // into before_delete_post and assume WC()->order_factory is available.
356 add_action(
357 'woocommerce_init',
358 function () use ( $locked_old, $locked_plugin_needs_upgrade, $locked_sync_needs_upgrade, $release_lock ) {
359 try {
360 $this->db_upgrade( $locked_old, VERSION );
361
362 // Report the upgrade only once the migration has actually
363 // completed — queueing it beside bump_versions() would claim a
364 // finished upgrade even when db_upgrade() threw or never ran.
365 // Still exactly-once: the version was bumped above, so the
366 // upgrade is not re-detected on the next request.
367 if ( $locked_plugin_needs_upgrade ) {
368 ( new Lifecycle_Events() )->record_upgrade( $locked_old, VERSION );
369 }
370 if ( $locked_sync_needs_upgrade && Sync_Api::SCHEMA_VERSION === get_option( Sync_Api::SCHEMA_OPTION, null ) ) {
371 ( new Sync_Journal() )->register_hooks();
372 ( new Integrity_Digest() )->register_hooks();
373 }
374 } finally {
375 $release_lock();
376 remove_action( 'shutdown', $release_lock );
377 }
378 }
379 );
380 }
381
382 /**
383 * Acquire the DB upgrade lock.
384 *
385 * @return bool True when this request owns the lock.
386 */
387 private function acquire_db_upgrade_lock(): bool {
388 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
389
390 return \WP_Upgrader::create_lock( self::DB_UPGRADE_LOCK_NAME, self::DB_UPGRADE_LOCK_TTL );
391 }
392
393 /**
394 * Release the DB upgrade lock.
395 */
396 private function release_db_upgrade_lock(): void {
397 if ( ! class_exists( '\WP_Upgrader', false ) ) {
398 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
399 }
400
401 \WP_Upgrader::release_lock( self::DB_UPGRADE_LOCK_NAME );
402 }
403
404 /**
405 * Plugin conflicts.
406 *
407 * - NextGEN Gallery is a terrible plugin. It buffers all content on 'init' action, priority -1 and inserts junk code.
408 */
409 private function plugin_check(): void {
410 // disable NextGEN Gallery resource manager
411 // if ( ! \defined( 'NGG_DISABLE_RESOURCE_MANAGER' ) ) {
412 // \define( 'NGG_DISABLE_RESOURCE_MANAGER', true );
413 // }.
414 }
415
416 /**
417 * Get all blog ids of blogs in the current network that are:
418 * - not archived
419 * - not spam
420 * - not deleted.
421 */
422 private function get_blog_ids() {
423 global $wpdb;
424
425 // get an array of blog ids.
426 $sql = "SELECT blog_id FROM $wpdb->blogs
427 WHERE archived = '0' AND spam = '0'
428 AND deleted = '0'";
429
430 return $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query, no user input
431 }
432
433 /**
434 * Get the role capability definition.
435 *
436 * @return array<string, array<string, bool>|array<int, string>> Role capabilities keyed by role.
437 */
438 private static function role_capability_definition(): array {
439 // WC 9.9 replaced promote_users with create_customers for customer creation.
440 $customer_create_cap = \defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '9.9', '>=' ) // @phpstan-ignore-line
441 ? 'create_customers'
442 : 'promote_users';
443
444 // Cashier role.
445 $cashier_capabilities = array(
446 'read' => true,
447 'read_private_products' => true,
448 'publish_products' => true,
449 'edit_product' => true,
450 'edit_products' => true,
451 'edit_published_products' => true,
452 'edit_private_products' => true,
453 'edit_others_products' => true,
454 'read_private_shop_orders' => true,
455 'publish_shop_orders' => true,
456 'edit_shop_orders' => true,
457 'edit_others_shop_orders' => true,
458 'list_users' => true,
459 $customer_create_cap => true,
460 'edit_users' => true,
461 'read_private_shop_coupons' => true,
462 'publish_shop_coupons' => true,
463 'edit_shop_coupons' => true,
464 'edit_published_shop_coupons' => true,
465 'edit_private_shop_coupons' => true,
466 'edit_others_shop_coupons' => true,
467 'manage_product_terms' => true,
468 );
469
470 return array(
471 'cashier' => $cashier_capabilities,
472 'administrator' => array(
473 'manage_woocommerce_pos',
474 'access_woocommerce_pos',
475 'edit_wcpos_store',
476 'read_wcpos_store',
477 'delete_wcpos_store',
478 'edit_wcpos_stores',
479 'edit_others_wcpos_stores',
480 'publish_wcpos_stores',
481 'read_private_wcpos_stores',
482 'delete_wcpos_stores',
483 'delete_private_wcpos_stores',
484 'delete_published_wcpos_stores',
485 'delete_others_wcpos_stores',
486 'edit_private_wcpos_stores',
487 'edit_published_wcpos_stores',
488 ),
489 'shop_manager' => array( 'manage_woocommerce_pos', 'access_woocommerce_pos' ),
490 );
491 }
492
493 /**
494 * Get the role-capabilities definition fingerprint.
495 */
496 private function role_caps_fingerprint(): string {
497 return md5( wp_json_encode( self::role_capability_definition() ) );
498 }
499
500 /**
501 * Add POS specific roles.
502 */
503 private function create_pos_roles(): void {
504 $role_capabilities = self::role_capability_definition();
505 $cashier_capabilities = $role_capabilities['cashier'];
506
507 add_role(
508 'cashier',
509 /* translators: Plugin activation notice label. */
510 __( 'Cashier', 'woocommerce-pos' ),
511 $cashier_capabilities
512 );
513
514 $obsolete_customer_create_cap = isset( $cashier_capabilities['create_customers'] ) ? 'promote_users' : 'create_customers';
515 $cashier = get_role( 'cashier' );
516 if ( $cashier ) {
517 $cashier->remove_cap( $obsolete_customer_create_cap );
518 }
519
520 // Sync all capabilities to the existing role. add_role() is a no-op when
521 // the role already exists, so capabilities added in newer versions would
522 // never reach existing installs without this.
523 $this->add_pos_capability(
524 array(
525 'cashier' => array_merge(
526 array( 'access_woocommerce_pos' ),
527 array_keys( $cashier_capabilities )
528 ),
529 )
530 );
531 }
532
533 /**
534 * Add default pos capabilities to administrator and shop_manager roles.
535 *
536 * @param array $roles An array of arrays representing the roles and their POS capabilities.
537 */
538 private function add_pos_capability( $roles ): void {
539 foreach ( $roles as $slug => $caps ) {
540 $role = get_role( $slug );
541 if ( $role ) {
542 foreach ( $caps as $cap ) {
543 $role->add_cap( $cap );
544 }
545 }
546 }
547 }
548
549 /**
550 * Upgrade database.
551 *
552 * @param string $old Old version.
553 * @param string $current Current version.
554 */
555 private function db_upgrade( $old, $current ): void {
556 $db_updates = array(
557 '0.4' => 'updates/update-0.4.php',
558 '0.4.6' => 'updates/update-0.4.6.php',
559 '1.0.0-beta.1' => 'updates/update-1.0.0-beta.1.php',
560 '1.6.1' => 'updates/update-1.6.1.php',
561 '1.8.0' => 'updates/update-1.8.0.php',
562 '1.8.7' => 'updates/update-1.8.7.php',
563 '1.8.12' => 'updates/update-1.8.12.php',
564 '1.8.13' => 'updates/update-1.8.13.php',
565 '1.9.0' => 'updates/update-1.9.0.php',
566 '1.10.0' => 'updates/update-1.10.0.php',
567 );
568 foreach ( $db_updates as $version => $updater ) {
569 if ( version_compare( $version, $old, '>' ) &&
570 version_compare( $version, $current, '<=' ) ) {
571 include $updater;
572 }
573 }
574
575 if ( Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null ) ) {
576 $this->install_sync_schema();
577 }
578 }
579
580 /**
581 * If \WCPOS\WooCommercePOSPro\ is installed, check the version is above MIN_PRO_VERSION.
582 */
583 private function pro_version_check(): void {
584 if ( class_exists( '\WCPOS\WooCommercePOSPro\Activator' ) ) {
585 if ( version_compare( \WCPOS\WooCommercePOSPro\VERSION, MIN_PRO_VERSION, '<' ) ) { // @phpstan-ignore-line
586
587 /*
588 * NOTE: the deactivate_plugins function is not available in the frontend or ajax
589 * This is an extreme situation where the Pro plugin could crash the site, so we need to deactivate it
590 */
591 if ( ! \function_exists( 'deactivate_plugins' ) ) {
592 require_once ABSPATH . '/wp-admin/includes/plugin.php';
593 }
594
595 // WCPOS Pro is activated, but the version is too low - use the constant for dynamic folder name.
596 deactivate_plugins( \WCPOS\WooCommercePOSPro\PLUGIN_FILE ); // @phpstan-ignore-line
597
598 // Defer __() call to avoid "too early" warning in WordPress 6.7+.
599 add_action(
600 'admin_init',
601 function () {
602 $message = \sprintf(
603 // translators: 1: WCPOS Pro URL, 2: Minimum Pro version, 3: Plugins URL.
604 __( '<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' ),
605 'https://wcpos.com/my-account',
606 MIN_PRO_VERSION,
607 admin_url( 'plugins.php' )
608 ) . ' &raquo;';
609
610 Admin\Notices::add( $message );
611 }
612 );
613 }
614 }
615 }
616 }
617