PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.16
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 / Services / Pos_Order_Audit.php

Pos_Order_Audit.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.16, at includes/Services/Pos_Order_Audit.php

221 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * POS order audit-meta authority.
4 *
5 * Single source of truth for the `_pos_*` audit keys (who rang up the sale,
6 * which store, the cash amounts) shared by the wcpos/v1 orders controller and
7 * the wcpos/v2 write controller. The server is the sole authoritative writer
8 * of this trail: `_pos_user` and `_woocommerce_pos_version` are always
9 * server-derived, and the till-sourced values (`_pos_store`, cash-tender) are
10 * accepted from the client payload only at create and only when valid.
11 *
12 * @package WCPOS\WooCommercePOS\Services
13 */
14
15 namespace WCPOS\WooCommercePOS\Services;
16
17 use WCPOS\WooCommercePOS\Sync\Meta_Entry;
18
19 /**
20 * Pos_Order_Audit service.
21 */
22 final class Pos_Order_Audit {
23 /**
24 * Audit keys the SERVER derives — a client-supplied value is never accepted.
25 *
26 * @var string[]
27 */
28 private const SERVER_META_KEYS = array( '_pos_user', '_pos_user_created', '_pos_payment_asserted', '_woocommerce_pos_version' );
29
30 /**
31 * Audit keys sourced from the till at the sale (client-supplied, validated,
32 * write-once).
33 *
34 * @var string[]
35 */
36 private const TILL_META_KEYS = array( '_pos_store', '_pos_cash_amount_tendered', '_pos_cash_change', '_pos_card_cashback' );
37
38 /**
39 * The subset of till keys that are monetary AMOUNTS (unsigned plain decimals);
40 * `_pos_store` is an identifier, not an amount.
41 *
42 * @var string[]
43 */
44 private const CASH_META_KEYS = array( '_pos_cash_amount_tendered', '_pos_cash_change', '_pos_card_cashback' );
45
46 /**
47 * The monetary till keys, for callers that stamp cash amounts
48 * (Write_Controller::stamp_order_till_meta on the v2 update path).
49 *
50 * @return string[]
51 */
52 public static function cash_meta_keys(): array {
53 return self::CASH_META_KEYS;
54 }
55
56 /**
57 * Every audit key: the server-derived and the till-sourced set.
58 *
59 * @return string[]
60 */
61 public static function audit_meta_keys(): array {
62 return array_merge( self::SERVER_META_KEYS, self::TILL_META_KEYS );
63 }
64
65 /**
66 * A `meta_data` array safe to apply on order CREATE: the server-derived keys
67 * are removed (the write path re-stamps them from the authenticated request),
68 * and till entries use the same last-entry-wins parser as the v2 create path.
69 *
70 * @param array $meta_data REST `meta_data` entries (arrays or objects with key/value).
71 *
72 * @return array
73 */
74 public static function sanitize_create_meta( array $meta_data ): array {
75 $sanitized = self::strip_audit_meta( $meta_data );
76 foreach ( self::till_meta_from_payload( $meta_data ) as $key => $value ) {
77 $sanitized[] = array(
78 'key' => $key,
79 'value' => $value,
80 );
81 }
82
83 return $sanitized;
84 }
85
86 /**
87 * A `meta_data` array with every audit key removed — used by the v1 update
88 * path and both v2 forwards. The audit trail is write-once at the sale: an
89 * edit under a different cashier/store (or a forged payload) must not
90 * rewrite it. (The one sanctioned exception is v2's explicit reassignment
91 * flow in Write_Controller, which re-stamps `_pos_user`/`_pos_store` with
92 * its own authorization checks and an order note.)
93 *
94 * WooCommerce resolves an entry by its `id` BEFORE its `key` and overwrites
95 * both the row's key and value — so an id-addressed entry under a harmless
96 * key would rename an audit row away. Pass the order's audit-row ids as
97 * `$protected_meta_ids` and such entries are dropped too.
98 *
99 * @param array $meta_data REST `meta_data` entries (arrays or objects with key/value).
100 * @param int[] $protected_meta_ids Existing audit-row meta ids on the target order (see audit_meta_ids()).
101 *
102 * @return array
103 */
104 public static function strip_audit_meta( array $meta_data, array $protected_meta_ids = array() ): array {
105 $strip = self::audit_meta_keys();
106
107 return array_values(
108 array_filter(
109 $meta_data,
110 static function ( $entry ) use ( $strip, $protected_meta_ids ) {
111 if ( \in_array( self::entry_key( $entry ), $strip, true ) ) {
112 return false;
113 }
114 $id = \is_array( $entry ) ? ( $entry['id'] ?? null ) : ( \is_object( $entry ) ? ( $entry->id ?? null ) : null );
115
116 return ! ( is_numeric( $id ) && \in_array( (int) $id, $protected_meta_ids, true ) );
117 }
118 )
119 );
120 }
121
122 /**
123 * Meta ids of the order's existing audit rows — the rows an id-addressed
124 * `meta_data` entry could target (see strip_audit_meta()).
125 *
126 * @param mixed $order A WC_Order (or false/null when lookup failed — safe no-op).
127 *
128 * @return int[]
129 */
130 public static function audit_meta_ids( $order ): array {
131 if ( ! \is_object( $order ) || ! method_exists( $order, 'get_meta_data' ) ) {
132 return array();
133 }
134 $keys = self::audit_meta_keys();
135 $ids = array();
136 foreach ( $order->get_meta_data() as $meta ) {
137 $data = \is_object( $meta ) && method_exists( $meta, 'get_data' ) ? $meta->get_data() : array();
138 if ( isset( $data['id'], $data['key'] ) && \in_array( (string) $data['key'], $keys, true ) ) {
139 $ids[] = (int) $data['id'];
140 }
141 }
142
143 return $ids;
144 }
145
146 /**
147 * The validated till key⇒value map from a client `meta_data` array (last
148 * entry wins for a repeated key, invalid values dropped). This is the one
149 * parser for till values — callers must not rebuild the extraction loop.
150 *
151 * @param array $meta_data REST `meta_data` entries (arrays or objects with key/value).
152 *
153 * @return array<string, string>
154 */
155 public static function till_meta_from_payload( array $meta_data ): array {
156 $client = array();
157 foreach ( $meta_data as $entry ) {
158 $key = self::entry_key( $entry );
159 if ( null !== $key ) {
160 $client[ $key ] = self::entry_value( $entry );
161 }
162 }
163 $till = array();
164 foreach ( self::TILL_META_KEYS as $key ) {
165 if ( array_key_exists( $key, $client ) && self::is_valid_till_value( $key, $client[ $key ] ) ) {
166 $till[ $key ] = (string) $client[ $key ];
167 }
168 }
169
170 return $till;
171 }
172
173 /**
174 * Whether a till value may persist: never an empty/non-scalar value, and the
175 * cash AMOUNTS must be unsigned plain decimals (a malformed amount would break
176 * Pro analytics aggregations). `_pos_store` is an identifier — the store-scope
177 * model allows numeric ids, uuids, or slugs — so any non-empty scalar is kept.
178 *
179 * @param string $key The till meta key.
180 * @param mixed $value The client-supplied value.
181 *
182 * @return bool
183 */
184 public static function is_valid_till_value( string $key, $value ): bool {
185 if ( ! \is_scalar( $value ) || '' === (string) $value ) {
186 return false;
187 }
188 if ( \in_array( $key, self::CASH_META_KEYS, true ) && 1 !== preg_match( '/^\d+(?:\.\d+)?$/', (string) $value ) ) {
189 return false;
190 }
191
192 return true;
193 }
194
195 /**
196 * The entry's meta key, or null for a malformed entry. A `key` that is an
197 * array/object must not be used for comparisons (PHP "Illegal offset type"
198 * territory) — treat it as unrecognized rather than crash the write.
199 *
200 * @param mixed $entry A REST `meta_data` entry.
201 *
202 * @return string|null
203 */
204 private static function entry_key( $entry ) {
205 $key = Meta_Entry::key( $entry );
206
207 return \is_scalar( $key ) ? (string) $key : null;
208 }
209
210 /**
211 * The entry's value ('' when absent or malformed).
212 *
213 * @param mixed $entry A REST `meta_data` entry.
214 *
215 * @return mixed
216 */
217 private static function entry_value( $entry ) {
218 return Meta_Entry::value( $entry ) ?? '';
219 }
220 }
221