PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.26 1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 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 All 170 releases
← All changes | includes/API/V2/Write_Controller.php +52 -56 1.10.2 → 1.10.17 View file →
@@ -7,8 +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\Customer_Account_Guard;
11 12 use WCPOS\WooCommercePOS\Services\Tax_Id_Types;
12 13 use WCPOS\WooCommercePOS\Sync\Api;
13 14 use WCPOS\WooCommercePOS\Sync\Collections;
14 15 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
@@ -43,8 +44,15 @@
43 44 * live in an injected mutation store, so the apply logic is unit-testable with a
44 45 * fake store + a stubbed `rest_do_request`.
45 46 */
46 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;
54 +
47 55 // Our gate (capability + F13 health); forwarded writes scope the client-tier grant below.
48 56 use Endpoint_Permissions;
49 57
50 58
@@ -433,58 +441,8 @@
433 441 return $this->envelope_document( $this->document_for( $meta, $new_id ), $m['recordId'], $meta, $new_id, $response->get_status(), $writer );
434 442 }
435 443
436 444 /**
437 - * The grace comparer (#423 step 2, option `woo_rxdb_sync_legacy_revision_grace`,
438 - * default on until retirement): on a canonical mismatch, accept a baseRevision
439 - * that matches the CURRENT document under a PRE-CUTOVER form —
440 - * - the legacy order sha256 (no ksort, volatiles included): same content,
441 - * old algorithm ⇒ the precondition is genuinely current;
442 - * - a pre-taxonomy-sort sha256 for non-orders: same content, previous
443 - * canonicalizer ⇒ queued writes survive the revision transition;
444 - * - a pre-1b lane synthesis (non-sha256 values the client fetchers stored
445 - * as sync.revision before the proxy revision stamp): date_modified_gmt
446 - * for order/post/user collections, String(id) for term collections
447 - * (deliberately vacuous — that lane never had optimistic concurrency;
448 - * grace honours the contract the record was written under).
449 - * A real conflict mismatches every form → 409 exactly as before. The ack
450 - * always returns the CANONICAL currentRevision, re-anchoring the client.
451 - */
452 - private function revision_matches_with_grace( $base, string $current_revision, array $meta, int $id, array $bare, bool $allow_term_grace = true ): bool {
453 - if ( $base === $current_revision ) {
454 - return true;
455 - }
456 - if ( ! is_string( $base ) || 'yes' !== get_option( 'woocommerce_pos_sync_legacy_revision_grace', 'yes' ) ) {
457 - return false;
458 - }
459 - if ( 0 === strpos( $base, 'sha256:' ) ) {
460 - if ( 'order' === ( $meta['id_type'] ?? '' ) && $id > 0 ) {
461 - if ( Order_Serializer::pre_augmentation_canonical_revision( $bare ) === $base
462 - || Order_Serializer::pre_item_uuid_canonical_revision( $bare ) === $base ) {
463 - return true;
464 - }
465 - $payload = ( new Order_Serializer() )->serialize_order( $id, new WP_REST_Request() );
466 - return Order_Serializer::legacy_revision( $payload ) === $base;
467 - }
468 - return Revision::pre_taxonomy_sort_revision( $bare ) === $base;
469 - }
470 - $id_type = $meta['id_type'] ?? '';
471 - if ( in_array( $id_type, array( 'order', 'post', 'user' ), true ) ) {
472 - // Read the date from wherever the document carries it. A variation's document is the
473 - // `{ id, parent_id, payload }` wrapper, so a top-level-only read makes this branch dead
474 - // code for the one collection whose canonical revision IS a date — and it would come
475 - // back to life, silently, the day the wrapper is dropped.
476 - $nested = isset( $bare['payload'] ) && is_array( $bare['payload'] ) ? $bare['payload'] : array();
477 - $date = (string) ( $bare['date_modified_gmt'] ?? $nested['date_modified_gmt'] ?? '' );
478 - return '' !== $date && $base === $date;
479 - }
480 - if ( 'term' === $id_type && $allow_term_grace ) {
481 - return (string) ( $bare['id'] ?? '' ) === $base;
482 - }
483 - return false;
484 - }
485 -
486 - /**
487 445 * The pre-CAS post-type capability gate shared by the update and delete paths.
488 446 *
489 447 * Only the WP-post-backed collections carry a Woo capability check of their own;
490 448 * every other collection is gated by the endpoint permission callback alone, so
@@ -542,9 +500,9 @@
542 500 return $current;
543 501 }
544 502 $current_bare = is_array( $current->get_data() ) ? $current->get_data() : array();
545 503 $current_revision = $this->revision_for( $meta, $id, $current_bare );
546 - if ( ! $this->revision_matches_with_grace( $m['baseRevision'], $current_revision, $meta, $id, $current_bare ) ) {
504 + if ( $m['baseRevision'] !== $current_revision ) {
547 505 return new WP_REST_Response(
548 506 array(
549 507 'code' => 'woo_rxdb_sync_conflict',
550 508 'message' => 'baseRevision is stale.',
@@ -607,9 +565,9 @@
607 565 return $current;
608 566 }
609 567 $current_bare = is_array( $current->get_data() ) ? $current->get_data() : array();
610 568 $current_revision = $this->revision_for( $meta, $id, $current_bare );
611 - if ( ! $this->revision_matches_with_grace( $m['baseRevision'], $current_revision, $meta, $id, $current_bare, false ) ) {
569 + if ( $m['baseRevision'] !== $current_revision ) {
612 570 return new WP_REST_Response(
613 571 array(
614 572 'code' => 'woo_rxdb_sync_conflict',
615 573 'message' => 'baseRevision is stale.',
@@ -809,10 +767,43 @@
809 767 }
810 768 }
811 769
812 770 /**
813 - * Authorize proxied catalog mutations for POS users.
771 + * Judge a customer edit or delete the way V1\Customers_Controller does.
814 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 + *
815 806 * This filter is attached only while a sync push is forwarded to wc/v3, so
816 807 * direct WooCommerce requests keep their normal permission checks.
817 808 *
818 809 * @param bool $permission The current permission.
@@ -822,8 +813,14 @@
822 813 *
823 814 * @return bool
824 815 */
825 816 public function wcpos_check_permissions( $permission, $context, $object_id, $post_type ) {
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 +
826 823 // Catalog and coupon WRITES require the user's real WooCommerce
827 824 // capabilities — no POS-tier widening. The cashier role is deliberately
828 825 // read-only on catalog (Activator), and a blanket grant here handed
829 826 // every POS user product deletion and coupon minting. Product decision
@@ -967,12 +964,11 @@
967 964 *
968 965 * This used to read `$bare['payload']['date_modified_gmt']` only, with `$bare['id']` as
969 966 * the fallback. Against a FLAT document that silently degrades to the variation's own
970 967 * ID — a value that never changes again. The failure would be total and invisible:
971 - * `revision_matches_with_grace()` would still let a queued date-based write through
972 - * (with the wrapper gone, its top-level `date_modified_gmt` branch finally resolves),
973 968 * the ack would hand the client the id as `currentRevision`, and from then on every
974 - * stale baseRevision would equal every recomputed one. Two tills editing the same
969 + * stale baseRevision would equal every recomputed one — the strict revision
970 + * comparison would pass every queued write. Two tills editing the same
975 971 * variation hours apart would both pass the precondition; the per-record lock would
976 972 * serialize them, so there would be no error — just a lost update, every time.
977 973 *
978 974 * The `$bare['id']` fallback is kept ONLY for a document carrying no date at all, and is