| @@ -98,11 +98,28 @@ | ||
| 98 | 98 | /** |
| 99 | 99 | * Fired when the plugin is activated. |
| 100 | 100 | * |
| 101 | 101 | * @param bool $install_sync_schema Whether to install the sync schema. |
| 102 | + * @param bool $full_role_sync Whether to repair all default role capabilities. | |
| 102 | 103 | */ |
| 103 | - public function single_activate( bool $install_sync_schema = true ): void { | |
| 104 | - $role_capabilities = self::role_capability_definition(); | |
| 104 | + public function single_activate( bool $install_sync_schema = true, bool $full_role_sync = true ): void { | |
| 105 | + $role_capabilities = self::role_capability_definition(); | |
| 106 | + $capability_names = $role_capabilities; | |
| 107 | + $capability_names['cashier'] = array_merge( array( 'access_woocommerce_pos' ), array_keys( $role_capabilities['cashier'] ) ); | |
| 108 | + $synced = get_option( 'woocommerce_pos_role_caps_synced', false ); | |
| 109 | + if ( ! $full_role_sync && false === $synced && get_option( 'woocommerce_pos_role_caps_fingerprint' ) === $this->role_caps_fingerprint() ) { | |
| 110 | + $synced = $capability_names; | |
| 111 | + } | |
| 112 | + // An upgrade grants only capabilities new to the definition since the | |
| 113 | + // last sync, so a capability the merchant removed on the Access screen | |
| 114 | + // stays removed. Explicit activation still repairs every default. | |
| 115 | + $granted = $capability_names; | |
| 116 | + if ( ! $full_role_sync && \is_array( $synced ) ) { | |
| 117 | + foreach ( $granted as $slug => $capabilities ) { | |
| 118 | + $already = isset( $synced[ $slug ] ) && \is_array( $synced[ $slug ] ) ? $synced[ $slug ] : array(); | |
| 119 | + $granted[ $slug ] = array_values( array_diff( $capabilities, $already ) ); | |
| 120 | + } | |
| 121 | + } | |
| 105 | 122 | |
| 106 | 123 | // Reseed the default template terms on the next request: (re)activation |
| 107 | 124 | // is the repair a merchant reaches for after deleting a term by hand. |
| 108 | 125 | // This also runs once per upgrade (version_check re-activates to sync |
| @@ -115,15 +132,15 @@ | ||
| 115 | 132 | self::autoload_request_latches(); |
| 116 | 133 | Admin\Permalink::ensure_default(); |
| 117 | 134 | |
| 118 | 135 | // create POS specific roles. |
| 119 | - $this->create_pos_roles(); | |
| 136 | + $this->create_pos_roles( $granted['cashier'] ); | |
| 120 | 137 | |
| 121 | 138 | // add pos capabilities to non POS roles. |
| 122 | 139 | $this->add_pos_capability( |
| 123 | 140 | array( |
| 124 | - 'administrator' => $role_capabilities['administrator'], | |
| 125 | - 'shop_manager' => $role_capabilities['shop_manager'], | |
| 141 | + 'administrator' => $granted['administrator'], | |
| 142 | + 'shop_manager' => $granted['shop_manager'], | |
| 126 | 143 | ) |
| 127 | 144 | ); |
| 128 | 145 | |
| 129 | 146 | $stored_roles = get_option( wp_roles()->role_key, array() ); |
| @@ -128,13 +145,14 @@ | ||
| 128 | 145 | |
| 129 | 146 | $stored_roles = get_option( wp_roles()->role_key, array() ); |
| 130 | 147 | $roles_are_persisted = is_array( $stored_roles ); |
| 131 | 148 | 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 ) { | |
| 149 | + foreach ( $granted as $slug => $capabilities ) { | |
| 150 | + if ( ! isset( $stored_roles[ $slug ] ) ) { | |
| 151 | + $roles_are_persisted = false; | |
| 152 | + break; | |
| 153 | + } | |
| 154 | + foreach ( $capabilities as $capability ) { | |
| 137 | 155 | if ( empty( $stored_roles[ $slug ]['capabilities'][ $capability ] ) ) { |
| 138 | 156 | $roles_are_persisted = false; |
| 139 | 157 | break 2; |
| 140 | 158 | } |
| @@ -143,8 +161,11 @@ | ||
| 143 | 161 | } |
| 144 | 162 | |
| 145 | 163 | $obsolete_customer_create_cap = isset( $role_capabilities['cashier']['create_customers'] ) ? 'promote_users' : 'create_customers'; |
| 146 | 164 | if ( $roles_are_persisted && empty( $stored_roles['cashier']['capabilities'][ $obsolete_customer_create_cap ] ) ) { |
| 165 | + // Snapshot first: a fingerprint that advanced past a failed snapshot | |
| 166 | + // write would never retry it. | |
| 167 | + update_option( 'woocommerce_pos_role_caps_synced', $capability_names, true ); | |
| 147 | 168 | update_option( 'woocommerce_pos_role_caps_fingerprint', $this->role_caps_fingerprint(), true ); |
| 148 | 169 | } |
| 149 | 170 | |
| 150 | 171 | // Flag the consent pop-up for the next admin page load. Done here |
| @@ -312,9 +333,10 @@ | ||
| 312 | 333 | $plugin_needs_upgrade = version_compare( $old, VERSION, '<' ); |
| 313 | 334 | $sync_needs_upgrade = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null ); |
| 314 | 335 | |
| 315 | 336 | $role_caps_fingerprint = $this->role_caps_fingerprint(); |
| 316 | - $role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $role_caps_fingerprint; | |
| 337 | + $role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $role_caps_fingerprint | |
| 338 | + || false === get_option( 'woocommerce_pos_role_caps_synced' ); | |
| 317 | 339 | if ( ! $plugin_needs_upgrade && ! $sync_needs_upgrade && ! $role_caps_need_sync ) { |
| 318 | 340 | return; |
| 319 | 341 | } |
| 320 | 342 | |
| @@ -326,9 +348,10 @@ | ||
| 326 | 348 | $locked_plugin_needs_upgrade = version_compare( $locked_old, VERSION, '<' ); |
| 327 | 349 | $locked_sync_needs_upgrade = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null ); |
| 328 | 350 | |
| 329 | 351 | $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; | |
| 352 | + $locked_role_caps_need_sync = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $locked_role_caps_fingerprint | |
| 353 | + || false === get_option( 'woocommerce_pos_role_caps_synced' ); | |
| 331 | 354 | if ( ! $locked_plugin_needs_upgrade && ! $locked_sync_needs_upgrade && ! $locked_role_caps_need_sync ) { |
| 332 | 355 | $this->release_db_upgrade_lock(); |
| 333 | 356 | return; |
| 334 | 357 | } |
| @@ -346,9 +369,9 @@ | ||
| 346 | 369 | // requires translations to be loaded (WordPress 6.7+). |
| 347 | 370 | add_action( |
| 348 | 371 | 'init', |
| 349 | 372 | function () { |
| 350 | - $this->single_activate( false ); | |
| 373 | + $this->single_activate( false, false ); | |
| 351 | 374 | } |
| 352 | 375 | ); |
| 353 | 376 | } |
| 354 | 377 | |
| @@ -513,10 +536,13 @@ | ||
| 513 | 536 | } |
| 514 | 537 | |
| 515 | 538 | /** |
| 516 | 539 | * Add POS specific roles. |
| 540 | + * | |
| 541 | + * @param string[]|null $capabilities Capability names to sync onto an existing role, or null for | |
| 542 | + * every default. A missing role is always created with the full set. | |
| 517 | 543 | */ |
| 518 | - private function create_pos_roles(): void { | |
| 544 | + private function create_pos_roles( ?array $capabilities = null ): void { | |
| 519 | 545 | $role_capabilities = self::role_capability_definition(); |
| 520 | 546 | $cashier_capabilities = $role_capabilities['cashier']; |
| 521 | 547 | |
| 522 | 548 | add_role( |
| @@ -522,9 +548,11 @@ | ||
| 522 | 548 | add_role( |
| 523 | 549 | 'cashier', |
| 524 | 550 | /* translators: Plugin activation notice label. */ |
| 525 | 551 | __( 'Cashier', 'woocommerce-pos' ), |
| 526 | - $cashier_capabilities | |
| 552 | + // A missing role is created whole, access gate included, whatever | |
| 553 | + // subset an incremental upgrade asked to sync. | |
| 554 | + array_merge( array( 'access_woocommerce_pos' => true ), $cashier_capabilities ) | |
| 527 | 555 | ); |
| 528 | 556 | |
| 529 | 557 | $obsolete_customer_create_cap = isset( $cashier_capabilities['create_customers'] ) ? 'promote_users' : 'create_customers'; |
| 530 | 558 | $cashier = get_role( 'cashier' ); |
| @@ -531,14 +559,14 @@ | ||
| 531 | 559 | if ( $cashier ) { |
| 532 | 560 | $cashier->remove_cap( $obsolete_customer_create_cap ); |
| 533 | 561 | } |
| 534 | 562 | |
| 535 | - // Sync all capabilities to the existing role. add_role() is a no-op when | |
| 563 | + // Sync the requested capabilities to the role. add_role() is a no-op when | |
| 536 | 564 | // the role already exists, so capabilities added in newer versions would |
| 537 | 565 | // never reach existing installs without this. |
| 538 | 566 | $this->add_pos_capability( |
| 539 | 567 | array( |
| 540 | - 'cashier' => array_merge( | |
| 568 | + 'cashier' => $capabilities ?? array_merge( | |
| 541 | 569 | array( 'access_woocommerce_pos' ), |
| 542 | 570 | array_keys( $cashier_capabilities ) |
| 543 | 571 | ), |
| 544 | 572 | ) |