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 / API / V2 / Digests_Controller.php

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

156 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS sync read surface.
4 *
5 * @package WCPOS\WooCommercePOS\API\V2
6 */
7
8 namespace WCPOS\WooCommercePOS\API\V2;
9
10 use WCPOS\WooCommercePOS\Sync\Api;
11 use WCPOS\WooCommercePOS\Sync\Collections;
12 use WCPOS\WooCommercePOS\Sync\Digest_Index;
13 use WCPOS\WooCommercePOS\Sync\Endpoint_Permissions;
14 use WP_REST_Controller;
15 use WP_REST_Request;
16 use WP_REST_Response;
17 use WP_REST_Server;
18
19 // phpcs:disable Squiz.Commenting, Generic.Commenting -- Ported lab documentation is preserved verbatim.
20
21 /**
22 * Leg-3 prime-pass digest endpoint (ADR 0014 increment 4c) — GET {API_NAMESPACE}/digests?include=<ids>.
23 *
24 * Returns each requested product/variation id's STORED 64-bit digest, with NO payload — the compact
25 * `{id, digest}` the client needs to backfill its existence-reconcile manifest for records that were
26 * already resident BEFORE Leg 3 shipped, so the first reconcile audit doesn't re-pull the whole catalog
27 * just to seed manifest rows. Digest-on-pull (#331/#332) covers records pulled AFTER Leg 3; this covers
28 * the pre-existing resident set. Servable ids with no stored digest remain absent; when `absence=explicit`,
29 * unservable ids are returned as `{id, deleted: true}` so the caller can prune authoritative absence.
30 */
31 final class Digests_Controller extends WP_REST_Controller {
32 use Endpoint_Permissions;
33
34 /**
35 * The digest store's read half — the readable-catalog scoping lives there so
36 * this endpoint and integrity/bucket can never disagree on what it means.
37 */
38 private Digest_Index $index;
39
40 public function __construct( ?Digest_Index $index = null ) {
41 $this->index = $index ?? new Digest_Index();
42 }
43
44 public function register_routes(): void {
45 register_rest_route(
46 Api::ROUTE_NAMESPACE,
47 '/digests',
48 array(
49 'methods' => WP_REST_Server::READABLE,
50 'callback' => array( $this, 'get_digests' ),
51 'permission_callback' => array( $this, 'permissions_check' ),
52 'args' => array(
53 'include' => array(
54 'required' => true,
55 'description' => 'Comma-separated ids to read stored digests for.',
56 ),
57 'collection' => array(
58 'default' => 'products',
59 'sanitize_callback' => 'sanitize_key',
60 'description' => "Which id-space: 'products' (default) or 'customers'.",
61 ),
62 'status' => array(
63 'sanitize_callback' => static function ( $status ) {
64 return 'publish' === $status ? 'publish' : '';
65 },
66 'description' => "Set to 'publish' to scope product digests to the readable catalog.",
67 ),
68 'absence' => array(
69 'sanitize_callback' => static function ( $absence ) {
70 return 'explicit' === $absence ? 'explicit' : '';
71 },
72 'description' => "Set to 'explicit' to return deleted rows for unservable ids.",
73 ),
74 ),
75 )
76 );
77 }
78
79 public function get_digests( WP_REST_Request $request ): WP_REST_Response {
80 $ids = $this->parse_ids( $request->get_param( 'include' ) );
81 if ( empty( $ids ) ) {
82 return new WP_REST_Response( array( 'digests' => array() ), 200 );
83 }
84 // Each collection has its own digest source + id-space (ADR 0015): 'customers' reads the wp_users
85 // customer digests; default 'products' reads the products/variations digests. The client boot
86 // prime uses this to backfill its per-id-space manifest.
87 $collection = $request->get_param( 'collection' );
88 $collection = \is_string( $collection ) ? $collection : 'products';
89 // Fail closed (#421 increment 8): only the registry's digest id-space
90 // OWNERS are servable — an unknown collection gets an explicit empty
91 // response, never the products digests under the wrong name.
92 if ( ! \array_key_exists( $collection, Collections::with( 'digest' ) ) ) {
93 return new WP_REST_Response(
94 array(
95 'digests' => array(),
96 'note' => \sprintf( 'collection "%s" has no digest id-space', $collection ),
97 ),
98 200
99 );
100 }
101 $read_ids = $ids;
102 if ( 'products' === $collection && 'publish' === $request->get_param( 'status' ) ) {
103 $read_ids = $this->index->published_product_ids( $ids );
104 }
105 $digests = $this->index->read_digests( $collection, $read_ids );
106 $explicit_absence = 'explicit' === $request->get_param( 'absence' );
107 $absent_ids = $explicit_absence ? array_values( array_diff( $ids, array_keys( $digests ) ) ) : array();
108 // Authoritative absence is the store's answer, not this endpoint's: the
109 // id-space, its live-row rule and the fail-open guard all live in
110 // Digest_Index, so this controller never learns the table shape.
111 $servable = array_fill_keys( $this->index->servable( $collection, $absent_ids ), true );
112 $out = array();
113 // Preserve request order; servable ids with no stored digest remain absent.
114 foreach ( $ids as $id ) {
115 if ( isset( $digests[ $id ] ) ) {
116 $out[] = array(
117 'id' => $id,
118 'digest' => $digests[ $id ],
119 );
120 } elseif ( $explicit_absence && ! isset( $servable[ $id ] ) ) {
121 $out[] = array(
122 'id' => $id,
123 'deleted' => true,
124 );
125 }
126 }
127
128 return new WP_REST_Response( array( 'digests' => $out ), 200 );
129 }
130
131 /**
132 * Accept `include` as a comma-separated string (?include=1,2,3) or an array; coerce to UNIQUE
133 * positive ints in request order. `Digest_Index::read_digests` re-sanitizes, but bounding here keeps a malformed
134 * query cheap and lets the response echo the caller's id ordering.
135 *
136 * @param mixed $include
137 */
138 private function parse_ids( $include ): array {
139 if ( \is_string( $include ) ) {
140 $include = explode( ',', $include );
141 }
142 if ( ! \is_array( $include ) ) {
143 return array();
144 }
145 $ids = array();
146 foreach ( $include as $value ) {
147 $id = (int) $value;
148 if ( $id > 0 ) {
149 $ids[ $id ] = $id; // dedupe, preserve first-seen order
150 }
151 }
152
153 return array_values( $ids );
154 }
155 }
156