| @@ -7,9 +7,9 @@ | ||
| 7 | 7 | |
| 8 | 8 | namespace WCPOS\WooCommercePOS\API\V2; |
| 9 | 9 | |
| 10 | 10 | use WCPOS\WooCommercePOS\API\V2\Writers\Collection_Writer_Resolver; |
| 11 | -use WCPOS\WooCommercePOS\Services\Permission_Rules; | |
| 11 | +use WCPOS\WooCommercePOS\Services\Customer_Account_Guard; | |
| 12 | 12 | use WCPOS\WooCommercePOS\Services\Tax_Id_Types; |
| 13 | 13 | use WCPOS\WooCommercePOS\Sync\Api; |
| 14 | 14 | use WCPOS\WooCommercePOS\Sync\Collections; |
| 15 | 15 | use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions; |
| @@ -44,8 +44,14 @@ | ||
| 44 | 44 | * live in an injected mutation store, so the apply logic is unit-testable with a |
| 45 | 45 | * fake store + a stubbed `rest_do_request`. |
| 46 | 46 | */ |
| 47 | 47 | class Write_Controller extends WP_REST_Controller { |
| 48 | + /** | |
| 49 | + * True while wc_rest_check_user_permissions() is re-run for a cleared target. | |
| 50 | + * | |
| 51 | + * @var bool | |
| 52 | + */ | |
| 53 | + private $rejudging_user_target = false; | |
| 48 | 54 | |
| 49 | 55 | // Our gate (capability + F13 health); forwarded writes scope the client-tier grant below. |
| 50 | 56 | use Endpoint_Permissions; |
| 51 | 57 | |
| @@ -593,13 +599,13 @@ | ||
| 593 | 599 | * |
| 594 | 600 | * @param int $id The order id. |
| 595 | 601 | */ |
| 596 | 602 | private function can_forward_delete( int $id ): bool { |
| 597 | - Permission_Rules::install_wc_filter(); | |
| 603 | + add_filter( 'woocommerce_rest_check_permissions', array( $this, 'wcpos_check_permissions' ), 10, 4 ); | |
| 598 | 604 | try { |
| 599 | 605 | return (bool) wc_rest_check_post_permissions( 'shop_order', 'delete', $id ); |
| 600 | 606 | } finally { |
| 601 | - Permission_Rules::uninstall_wc_filter(); | |
| 607 | + remove_filter( 'woocommerce_rest_check_permissions', array( $this, 'wcpos_check_permissions' ), 10 ); | |
| 602 | 608 | } |
| 603 | 609 | } |
| 604 | 610 | |
| 605 | 611 | private function checkpoint_and_finalize( string $mutation_id, int $remote_id, int $response_status ) { |
| @@ -745,9 +751,9 @@ | ||
| 745 | 751 | */ |
| 746 | 752 | private function dispatch_write( WP_REST_Request $request ) { |
| 747 | 753 | // Stamp here so direct callers (notably deletes) carry the scope too. |
| 748 | 754 | Store_Scope::stamp( $request ); |
| 749 | - Permission_Rules::install_wc_filter(); | |
| 755 | + add_filter( 'woocommerce_rest_check_permissions', array( $this, 'wcpos_check_permissions' ), 10, 4 ); | |
| 750 | 756 | try { |
| 751 | 757 | // Marked as OUR traffic for the duration of the forward, so a consumer |
| 752 | 758 | // keyed on store scope can act on a till write without also claiming |
| 753 | 759 | // every stock wc/v3 product write on the site (pro#425 review). |
| @@ -756,15 +762,104 @@ | ||
| 756 | 762 | return rest_do_request( $request ); |
| 757 | 763 | } |
| 758 | 764 | ); |
| 759 | 765 | } finally { |
| 760 | - Permission_Rules::uninstall_wc_filter(); | |
| 766 | + remove_filter( 'woocommerce_rest_check_permissions', array( $this, 'wcpos_check_permissions' ), 10 ); | |
| 761 | 767 | } |
| 762 | 768 | } |
| 763 | 769 | |
| 764 | - /** @deprecated Use Permission_Rules::wc_filter(). */ | |
| 770 | + /** | |
| 771 | + * Judge a customer edit or delete the way V1\Customers_Controller does. | |
| 772 | + * | |
| 773 | + * The staff guard runs first and is final. A target it has cleared is then | |
| 774 | + * re-judged by WooCommerce with the target's own roles allowed through the | |
| 775 | + * shop_manager role-name restriction, so a shop manager can edit a subscriber | |
| 776 | + * from a current app exactly as from the legacy route. WooCommerce's | |
| 777 | + * credential fence is untouched: it runs in the controller, not here. | |
| 778 | + * | |
| 779 | + * @param bool $permission WooCommerce's verdict so far. | |
| 780 | + * @param string $context 'edit' or 'delete'. | |
| 781 | + * @param int $target_id Target user ID. | |
| 782 | + */ | |
| 783 | + private function check_user_permission( bool $permission, string $context, int $target_id ): bool { | |
| 784 | + if ( $this->rejudging_user_target ) { | |
| 785 | + return $permission; | |
| 786 | + } | |
| 787 | + if ( ! Customer_Account_Guard::can_modify( get_current_user_id(), $target_id ) ) { | |
| 788 | + return false; | |
| 789 | + } | |
| 790 | + if ( $permission ) { | |
| 791 | + return true; | |
| 792 | + } | |
| 793 | + $this->rejudging_user_target = true; | |
| 794 | + $restore = Customer_Account_Guard::allow_target_roles( $target_id ); | |
| 795 | + try { | |
| 796 | + return (bool) wc_rest_check_user_permissions( $context, $target_id ); | |
| 797 | + } finally { | |
| 798 | + $restore(); | |
| 799 | + $this->rejudging_user_target = false; | |
| 800 | + } | |
| 801 | + } | |
| 802 | + | |
| 803 | + /** | |
| 804 | + * Authorize proxied mutations for POS users while protecting staff accounts. | |
| 805 | + * | |
| 806 | + * This filter is attached only while a sync push is forwarded to wc/v3, so | |
| 807 | + * direct WooCommerce requests keep their normal permission checks. | |
| 808 | + * | |
| 809 | + * @param bool $permission The current permission. | |
| 810 | + * @param string $context The request context. | |
| 811 | + * @param int $object_id The object ID. | |
| 812 | + * @param string $post_type The object type passed by WooCommerce. | |
| 813 | + * | |
| 814 | + * @return bool | |
| 815 | + */ | |
| 765 | 816 | public function wcpos_check_permissions( $permission, $context, $object_id, $post_type ) { |
| 766 | - return Permission_Rules::wc_filter( $permission, $context, $object_id, $post_type, 'writes' ); | |
| 817 | + // Customer edits/deletes: the staff guard is final, then a cleared target | |
| 818 | + // is judged by WooCommerce the same way the v1 controller judges it. | |
| 819 | + if ( 'user' === $post_type && (int) $object_id > 0 && \in_array( $context, array( 'edit', 'delete' ), true ) ) { | |
| 820 | + return $this->check_user_permission( (bool) $permission, $context, (int) $object_id ); | |
| 821 | + } | |
| 822 | + | |
| 823 | + // Catalog and coupon WRITES require the user's real WooCommerce | |
| 824 | + // capabilities — no POS-tier widening. The cashier role is deliberately | |
| 825 | + // read-only on catalog (Activator), and a blanket grant here handed | |
| 826 | + // every POS user product deletion and coupon minting. Product decision | |
| 827 | + // 2026-08-06: strict wc/v3 parity for catalog mutations; only the | |
| 828 | + // HPOS placeholder remap below (orders) adjusts anything, and it never | |
| 829 | + // grants beyond the user's own role caps. | |
| 830 | + | |
| 831 | + // Orders: with HPOS enabled (sync off), get_post() yields shop_order_placehold | |
| 832 | + // (map_meta_cap = false, no capability_type), so WooCommerce's REST check maps | |
| 833 | + // to the generic edit_post/delete_post caps that cashier-tier roles lack — | |
| 834 | + // even though they hold the real shop_orders caps. Re-check the capability the | |
| 835 | + // mapping SHOULD have produced, mirroring V1\Orders_Controller's | |
| 836 | + // update_item_permissions_check fix. No grant beyond the user's own role caps. | |
| 837 | + if ( ! $permission && 'shop_order' === $post_type ) { | |
| 838 | + $order_caps = array( | |
| 839 | + 'read' => 'read_private_shop_orders', | |
| 840 | + 'create' => 'publish_shop_orders', | |
| 841 | + 'delete' => 'delete_shop_orders', | |
| 842 | + ); | |
| 843 | + $order_cap = $order_caps[ $context ] ?? null; | |
| 844 | + // edit and delete are ownership-sensitive: the base *_shop_orders cap only | |
| 845 | + // authorizes acting on the user's OWN orders. Touching another user's order | |
| 846 | + // additionally requires the *_others_shop_orders cap, mirroring WooCommerce's | |
| 847 | + // own meta-cap map. Without this, a cashier with delete_shop_orders (but not | |
| 848 | + // delete_others_shop_orders) could delete/void orders they do not own. | |
| 849 | + if ( \in_array( $context, array( 'edit', 'delete' ), true ) ) { | |
| 850 | + $order_post = get_post( $object_id ); | |
| 851 | + if ( $order_post ) { | |
| 852 | + $owns_order = get_current_user_id() === (int) $order_post->post_author; | |
| 853 | + $order_cap = $owns_order ? "{$context}_shop_orders" : "{$context}_others_shop_orders"; | |
| 854 | + } | |
| 855 | + } | |
| 856 | + if ( $order_cap && current_user_can( $order_cap ) ) { | |
| 857 | + $permission = true; | |
| 858 | + } | |
| 859 | + } | |
| 860 | + | |
| 861 | + return $permission; | |
| 767 | 862 | } |
| 768 | 863 | |
| 769 | 864 | /** |
| 770 | 865 | * Read this collection's document for one record, through its writer. |