PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
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 1.9.13 All 162 releases
woocommerce-pos / includes / API / V2 / Digests_Controller.php

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

157 lines 5.8 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 $published_ids = Collections::row( $collection )['digest']['published_ids'] ?? null;
103 if ( null !== $published_ids && 'publish' === $request->get_param( 'status' ) ) {
104 $read_ids = $this->index->$published_ids( $ids );
105 }
106 $digests = $this->index->read_digests( $collection, $read_ids );
107 $explicit_absence = 'explicit' === $request->get_param( 'absence' );
108 $absent_ids = $explicit_absence ? array_values( array_diff( $ids, array_keys( $digests ) ) ) : array();
109 // Authoritative absence is the store's answer, not this endpoint's: the
110 // id-space, its live-row rule and the fail-open guard all live in
111 // Digest_Index, so this controller never learns the table shape.
112 $servable = array_fill_keys( $this->index->servable( $collection, $absent_ids ), true );
113 $out = array();
114 // Preserve request order; servable ids with no stored digest remain absent.
115 foreach ( $ids as $id ) {
116 if ( isset( $digests[ $id ] ) ) {
117 $out[] = array(
118 'id' => $id,
119 'digest' => $digests[ $id ],
120 );
121 } elseif ( $explicit_absence && ! isset( $servable[ $id ] ) ) {
122 $out[] = array(
123 'id' => $id,
124 'deleted' => true,
125 );
126 }
127 }
128
129 return new WP_REST_Response( array( 'digests' => $out ), 200 );
130 }
131
132 /**
133 * Accept `include` as a comma-separated string (?include=1,2,3) or an array; coerce to UNIQUE
134 * positive ints in request order. `Digest_Index::read_digests` re-sanitizes, but bounding here keeps a malformed
135 * query cheap and lets the response echo the caller's id ordering.
136 *
137 * @param mixed $include
138 */
139 private function parse_ids( $include ): array {
140 if ( \is_string( $include ) ) {
141 $include = explode( ',', $include );
142 }
143 if ( ! \is_array( $include ) ) {
144 return array();
145 }
146 $ids = array();
147 foreach ( $include as $value ) {
148 $id = (int) $value;
149 if ( $id > 0 ) {
150 $ids[ $id ] = $id; // dedupe, preserve first-seen order
151 }
152 }
153
154 return array_values( $ids );
155 }
156 }
157