PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.21
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.21
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 1.10.2 1.10.1 1.10.0 All 167 releases
← All changes | includes/Services/Permission_Rules.php +106 -10 1.10.19 → 1.10.21 View file →
@@ -133,8 +133,16 @@
133 133 }
134 134 if ( 'v1' === $lane && 'orders' === $collection && is_wp_error( $permission ) && self::wc_filter( false, $context, $object_id, 'shop_order', 'orders', 'v1' ) ) {
135 135 return true;
136 136 }
137 + // The implicit v1 scope passes WooCommerce's grant through; ownership still rules.
138 + if ( 'v1' === $lane && 'orders' === $collection && true === $permission && ! self::wc_filter( true, $context, $object_id, 'shop_order', 'orders', 'v1' ) ) {
139 + return new \WP_Error(
140 + "woocommerce_rest_cannot_{$context}",
141 + __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ),
142 + array( 'status' => rest_authorization_required_code() )
143 + );
144 + }
137 145 return $permission;
138 146 } finally {
139 147 if ( $restore ) {
140 148 $restore();
@@ -215,28 +223,34 @@
215 223 self::$rejudging_user_target = false;
216 224 }
217 225 }
218 226 }
219 - if ( ! $permission && 'shop_order' === $post_type && in_array( $collection, array( 'writes', 'orders' ), true ) ) {
227 + if ( 'shop_order' === $post_type && in_array( $collection, array( 'writes', 'orders' ), true ) ) {
228 + $ownership = self::ownership_applies( $lane, $context );
229 + $order = $ownership ? wc_get_order( $object_id ) : false;
230 + if ( $permission ) {
231 + // WooCommerce granted on its own signal — under HPOS with compatibility
232 + // sync the post author is whoever created the order — but the cashier the
233 + // order is assigned to is the owner, so a reassigned order still needs the
234 + // `others` capability from its creator.
235 + if ( $order instanceof \WC_Abstract_Order && ! self::owns_order( $order ) && ! current_user_can( "{$context}_others_shop_orders" ) ) {
236 + $permission = false;
237 + }
238 + return $permission;
239 + }
220 240 // V1 checked existence before its fallback (23defd774); v2 did not.
221 241 if ( 'v1' === $lane && ( ! wc_get_order( $object_id ) || ! current_user_can( "{$context}_shop_orders" ) ) ) {
222 242 return $permission;
223 243 }
224 - // Without a post row, only v1 historically granted the flat edit cap.
244 + // Edit is always ownership-aware (ORDER_RULES), so it has no flat fallback.
225 245 $caps = array(
226 246 'read' => 'read_private_shop_orders',
227 247 'create' => 'publish_shop_orders',
228 - 'edit' => 'v1' === $lane ? 'edit_shop_orders' : null,
229 248 'delete' => 'delete_shop_orders',
230 249 );
231 250 $cap = $caps[ $context ] ?? null;
232 - foreach ( self::ORDER_RULES as $rule ) {
233 - if ( $lane === $rule['lane'] && $context === $rule['context'] && $rule['ownership'] ) {
234 - $post = get_post( $object_id );
235 - if ( $post ) {
236 - $cap = get_current_user_id() === (int) $post->post_author ? "{$context}_shop_orders" : "{$context}_others_shop_orders";
237 - }
238 - }
251 + if ( $order instanceof \WC_Abstract_Order ) {
252 + $cap = self::owns_order( $order ) ? "{$context}_shop_orders" : "{$context}_others_shop_orders";
239 253 }
240 254 if ( $cap && current_user_can( $cap ) ) {
241 255 $permission = true;
242 256 }
@@ -248,8 +262,47 @@
248 262 return $permission;
249 263 }
250 264
251 265 /**
266 + * Whether ORDER_RULES makes this lane's context ownership-aware.
267 + *
268 + * @param string $lane Permission lane.
269 + * @param string $context Permission context.
270 + * @return bool
271 + */
272 + private static function ownership_applies( string $lane, string $context ): bool {
273 + foreach ( self::ORDER_RULES as $rule ) {
274 + if ( $lane === $rule['lane'] && $context === $rule['context'] ) {
275 + return (bool) $rule['ownership'];
276 + }
277 + }
278 +
279 + return false;
280 + }
281 +
282 + /**
283 + * Whether the current user is the cashier an order is assigned to.
284 + *
285 + * `_pos_user` is the only ownership signal an order carries on both storage
286 + * modes: the write lanes stamp it server-side on creation and move it on a
287 + * cashier reassignment. `post_author` is not that signal — the posts store
288 + * writes `1` for every order, and the HPOS placeholder row inherits whoever
289 + * was logged in when it was inserted (the customer, or nobody, for a web
290 + * order). An order without `_pos_user` (a web order) belongs to no cashier,
291 + * so it needs the `*_others_shop_orders` capability.
292 + *
293 + * @param \WC_Abstract_Order $order Order being judged.
294 + * @return bool
295 + */
296 + private static function owns_order( \WC_Abstract_Order $order ): bool {
297 + $cashier = $order->get_meta( '_pos_user' );
298 + $actor = get_current_user_id();
299 +
300 + // The lanes stamp the canonical decimal string, so an exact match is the strict test.
301 + return $actor > 0 && is_scalar( $cashier ) && (string) $cashier === (string) $actor;
302 + }
303 +
304 + /**
252 305 * Get the capabilities that identify protected staff accounts.
253 306 *
254 307 * @return array
255 308 */
@@ -341,8 +394,51 @@
341 394
342 395 return static function () use ( $filter ): void {
343 396 remove_filter( 'woocommerce_shop_manager_editable_roles', $filter );
344 397 };
398 + }
399 +
400 + /**
401 + * Apply the staff-account rule (#1918) wherever WordPress checks a user edit.
402 + *
403 + * Filters `map_meta_cap` so a till user below shop manager (holds
404 + * `access_woocommerce_pos` but not `manage_woocommerce`) may edit, delete,
405 + * remove or promote another account only when can_modify() allows it, on core,
406 + * wc/v3 and wp-admin routes as well as the POS lanes. Promoting oneself needs
407 + * `manage_options`. It only ever adds `do_not_allow`; it never grants.
408 + *
409 + * @param array $caps Primitive capabilities required.
410 + * @param string $cap Capability being checked.
411 + * @param int $user_id Acting user ID.
412 + * @param array $args Extra arguments; `$args[0]` is the target user ID.
413 + *
414 + * @return array
415 + */
416 + public static function map_user_meta_caps( $caps, $cap, $user_id, $args ): array {
417 + $caps = (array) $caps;
418 + if ( ! \in_array( $cap, array( 'edit_user', 'delete_user', 'remove_user', 'promote_user', 'edit_users', 'delete_users', 'promote_users' ), true ) ) {
419 + return $caps;
420 + }
421 + if ( ! isset( $args[0] ) || ! is_numeric( $args[0] ) || (int) $args[0] < 1 ) {
422 + return $caps;
423 + }
424 + $actor = (int) $user_id;
425 + if ( ! user_can( $actor, 'access_woocommerce_pos' ) || user_can( $actor, 'manage_woocommerce' ) ) {
426 + return $caps;
427 + }
428 + $target = (int) $args[0];
429 + if ( $actor === $target ) {
430 + if ( \in_array( $cap, array( 'promote_user', 'promote_users' ), true ) && ! user_can( $actor, 'manage_options' ) ) {
431 + $caps[] = 'do_not_allow';
432 + }
433 +
434 + return $caps;
435 + }
436 + if ( ! self::can_modify( $actor, $target ) ) {
437 + $caps[] = 'do_not_allow';
438 + }
439 +
440 + return $caps;
345 441 }
346 442
347 443 /**
348 444 * Build the staff account permission error.