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 / Services / Cashier.php

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

283 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cashier.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\Services;
9
10 use WCPOS\WooCommercePOS\Abstracts\Store;
11 use WCPOS\WooCommercePOS\Services\Settings\Access_Section;
12 use WCPOS\WooCommercePOS\Sync\Pos_Uuid;
13 use WP_User;
14
15 /**
16 * Cashier Service class.
17 */
18 class Cashier {
19 /**
20 * The single instance of the class.
21 *
22 * @var null|Cashier
23 */
24 private static $instance = null;
25
26 /**
27 * Constructor is private to prevent direct instantiation.
28 * Use Cashier::instance() instead.
29 */
30 private function __construct() {
31 }
32
33 /**
34 * Gets the singleton instance.
35 *
36 * @return Cashier
37 */
38 public static function instance(): self {
39 if ( null === self::$instance ) {
40 self::$instance = new self();
41 }
42
43 return self::$instance;
44 }
45
46 /**
47 * Get cashier UUID.
48 *
49 * Delegates to Pos_Uuid — the sole authority for `_woocommerce_pos_uuid` — so
50 * the /cashier endpoint and auth payloads serve the SAME identity as the
51 * /customers endpoint. The POS client keys its RxDB documents on this uuid, so
52 * a divergent value makes one person appear as two. Legacy multisite per-blog
53 * uuids (minted by an old version of this method) are adopted network-wide by
54 * the authority.
55 *
56 * @param WP_User $user User object.
57 *
58 * @return string UUID for the cashier ('' only if WooCommerce customer data is unavailable).
59 */
60 public function get_cashier_uuid( WP_User $user ): string {
61 return Pos_Uuid::ensure_user_uuid( $user );
62 }
63
64 /**
65 * Get cashier data for API responses.
66 *
67 * @param WP_User $user User object.
68 * @param bool $include_stores Whether to include stores data.
69 *
70 * @return array Cashier data.
71 */
72 public function get_cashier_data( WP_User $user, bool $include_stores = true ): array {
73 $uuid = $this->get_cashier_uuid( $user );
74 $last_access = get_user_meta( $user->ID, '_woocommerce_pos_last_access', true );
75
76 $data = array(
77 'uuid' => $uuid,
78 'id' => $user->ID,
79 'username' => $user->user_login,
80 'first_name' => $user->first_name,
81 'last_name' => $user->last_name,
82 'email' => $user->user_email,
83 'display_name' => $user->display_name,
84 'nice_name' => $user->user_nicename,
85 'roles' => array_values( $user->roles ),
86 // The helper reports effective grants, including role-editor denies.
87 'capabilities' => Access_Section::effective_capabilities( $user ),
88 'last_access' => $last_access ? $last_access : '',
89 'avatar_url' => get_avatar_url( $user->ID ),
90 );
91
92 if ( $include_stores ) {
93 $stores = $this->get_accessible_stores( $user );
94 $stores_data = array();
95 foreach ( $stores as $store ) {
96 $stores_data[] = $store->get_data();
97 }
98 $data['stores'] = $stores_data;
99 }
100
101 /*
102 * Filter cashier data.
103 *
104 * @param array $data Cashier data.
105 * @param WP_User $user User object.
106 * @param bool $include_stores Whether stores were included.
107 */
108 return apply_filters( 'woocommerce_pos_cashier_data', $data, $user, $include_stores );
109 }
110
111 /**
112 * Get stores accessible by the cashier.
113 *
114 * @TODO - This currently returns all stores. In the future, this should be
115 * customized based on user meta, roles, or other authorization logic to
116 * return only the stores the cashier is authorized to access.
117 *
118 * @param WP_User $user User object.
119 *
120 * @return array Array of Store objects.
121 */
122 public function get_accessible_stores( WP_User $user ): array {
123 $stores = wcpos_get_stores();
124
125 /*
126 * Filter stores accessible by cashier.
127 *
128 * @param array $stores Array of Store objects.
129 * @param WP_User $user User object.
130 */
131 return apply_filters( 'woocommerce_pos_cashier_accessible_stores', $stores, $user );
132 }
133
134 /**
135 * Check if a cashier has access to a specific store.
136 *
137 * @param WP_User $user User object.
138 * @param int $store_id Store ID.
139 *
140 * @return bool True if cashier has access, false otherwise.
141 */
142 public function has_store_access( WP_User $user, int $store_id ): bool {
143 $accessible_stores = $this->get_accessible_stores( $user );
144
145 foreach ( $accessible_stores as $store ) {
146 if ( $store->get_id() === $store_id ) {
147 return true;
148 }
149 }
150
151 return false;
152 }
153
154 /**
155 * Get a specific store for a cashier if they have access.
156 *
157 * @param WP_User $user User object.
158 * @param int $store_id Store ID.
159 *
160 * @return null|Store Store object if accessible, null otherwise.
161 */
162 public function get_accessible_store( WP_User $user, int $store_id ): ?Store {
163 $accessible_stores = $this->get_accessible_stores( $user );
164
165 foreach ( $accessible_stores as $store ) {
166 if ( $store->get_id() === $store_id ) {
167 return $store;
168 }
169 }
170
171 return null;
172 }
173
174 /**
175 * Update cashier's last access time.
176 *
177 * @param WP_User $user User object.
178 * @param string $timestamp Optional timestamp, defaults to current time.
179 *
180 * @return bool True on success, false on failure.
181 */
182 public function update_last_access( WP_User $user, string $timestamp = '' ): bool {
183 if ( empty( $timestamp ) ) {
184 $timestamp = current_time( 'mysql' );
185 }
186
187 return update_user_meta( $user->ID, '_woocommerce_pos_last_access', $timestamp );
188 }
189
190 /**
191 * Check if user has cashier permissions.
192 *
193 * @param WP_User $user User object.
194 *
195 * @return bool True if user has cashier permissions.
196 */
197 public function has_cashier_permissions( WP_User $user ): bool {
198 return user_can( $user, 'publish_shop_orders' );
199 }
200
201 /**
202 * POS baseline capabilities the user lacks.
203 *
204 * The baseline is what the POS needs to open and take a sale: the access gate,
205 * the cashier gate (publish_shop_orders — see has_cashier_permissions()), and the
206 * three reads every screen makes. Missing entries are reported, in this order,
207 * so a merchant can see which role or role-editor deny is responsible.
208 *
209 * @param WP_User $user User to check.
210 * @return string[] Missing capability names.
211 */
212 public function missing_pos_capabilities( WP_User $user ): array {
213 $baseline = array( 'access_woocommerce_pos', 'publish_shop_orders', 'read_private_products', 'read_private_shop_orders', 'list_users' );
214
215 return array_values( array_filter( $baseline, fn( $cap ) => ! user_can( $user, $cap ) ) );
216 }
217
218 /**
219 * Whether the user clears the two gates the server already enforces.
220 *
221 * Only access_woocommerce_pos (the REST gate) and publish_shop_orders (the
222 * cashier gate) block entry; a user missing only a read capability can still
223 * work partially, and the corrected capability payload tells the client what
224 * is missing.
225 *
226 * @param WP_User $user User to check.
227 * @return bool True when both entry capabilities are granted.
228 */
229 public function can_open_pos( WP_User $user ): bool {
230 $blocking = array_intersect( array( 'access_woocommerce_pos', 'publish_shop_orders' ), $this->missing_pos_capabilities( $user ) );
231
232 return empty( $blocking );
233 }
234
235 /**
236 * Describe missing baseline capabilities and how to grant them.
237 *
238 * @param WP_User $user User to check.
239 * @return string Diagnostic message, or empty unless the user is blocked by can_open_pos().
240 */
241 public function missing_pos_capabilities_message( WP_User $user ): string {
242 if ( $this->can_open_pos( $user ) ) {
243 return '';
244 }
245 $missing = $this->missing_pos_capabilities( $user );
246 /* translators: %s: Comma-separated missing capability names. */
247 $message = sprintf( __( 'This account cannot use the POS. Missing capabilities: %s.', 'woocommerce-pos' ), implode( ', ', $missing ) );
248 if ( count( $user->roles ) >= 2 ) {
249 $role_names = array_map(
250 function ( $slug ) {
251 $roles = wp_roles()->roles;
252
253 return isset( $roles[ $slug ]['name'] ) ? translate_user_role( $roles[ $slug ]['name'] ) : $slug;
254 },
255 $user->roles
256 );
257 /* translators: %s: Comma-separated role names. */
258 return $message . ' ' . sprintf( __( 'It has the roles %s. A capability denied on one role can override a grant from another, and role-editor plugins such as Members apply that deny first. Remove the extra role or clear the deny in the role editor.', 'woocommerce-pos' ), implode( ', ', $role_names ) );
259 }
260
261 /* translators: Guidance for granting missing POS capabilities. */
262 return $message . ' ' . __( 'Grant them under WCPOS Settings, Access, or assign a role that has them.', 'woocommerce-pos' );
263 }
264
265 /**
266 * Validate cashier access for API endpoints.
267 *
268 * @param int $current_user_id Current user ID.
269 * @param int $requested_id Requested cashier ID.
270 *
271 * @return bool True if access is allowed.
272 */
273 public function validate_cashier_access( int $current_user_id, int $requested_id ): bool {
274 // Users can access their own data.
275 if ( $current_user_id === $requested_id ) {
276 return true;
277 }
278
279 // Administrators can access any cashier data.
280 return current_user_can( 'manage_woocommerce' );
281 }
282 }
283