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.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 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / Services / Settings / Access_Section.php

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

312 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Access Settings Section.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\Services\Settings;
9
10 use WCPOS\WooCommercePOS\Interfaces\Settings_Section_Interface;
11 use WP_Error;
12 use WP_User;
13
14 /**
15 * The Access Settings Section.
16 *
17 * Non-option-backed: read() computes role capabilities from $wp_roles;
18 * write() mutates role capabilities via WP_Role::add_cap()/remove_cap().
19 * There is no woocommerce_pos_settings_access option.
20 */
21 class Access_Section implements Settings_Section_Interface {
22 /**
23 * Section id.
24 *
25 * @return string
26 */
27 public function id(): string {
28 return 'access';
29 }
30
31 /**
32 * No option-backed defaults for this section.
33 *
34 * @return array
35 */
36 public function defaults(): array {
37 return array();
38 }
39
40 /**
41 * Get the flat list of capability names.
42 *
43 * @return array
44 */
45 public static function capability_names(): array {
46 $caps = self::get_caps();
47
48 return array_merge( $caps['wcpos'], $caps['wc'], $caps['wp'] );
49 }
50
51 /**
52 * Capabilities the user can actually exercise, in the Access-settings vocabulary.
53 *
54 * The user_can() check is what every REST permission callback asks, so it is the
55 * answer the client must be given; it also runs the `user_has_cap` filter that
56 * role editors such as Members use to make a Deny on one role override a grant
57 * on another. The two singular meta caps (edit_product, delete_product) cannot
58 * go through user_can() without a post, so they are read from allcaps after the
59 * same filter has run.
60 *
61 * @param WP_User $user User to report on.
62 *
63 * @return string[] Capability names, in capability_names() order.
64 */
65 public static function effective_capabilities( WP_User $user ): array {
66 $names = self::capability_names();
67 // A multisite super admin holds every capability before the filter runs
68 // (WP_User::has_cap), so mirror that bypass for the two filtered names.
69 $super_admin = is_multisite() && is_super_admin( $user->ID );
70
71 return array_values(
72 array_filter(
73 $names,
74 function ( $cap ) use ( $user, $super_admin ) {
75 if ( ! in_array( $cap, array( 'edit_product', 'delete_product' ), true ) ) {
76 return user_can( $user, $cap );
77 }
78 if ( $super_admin ) {
79 return true;
80 }
81 // Same argument shape as WP_User::has_cap(): the caps being
82 // checked, then the requested cap and user id.
83 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core capability filter, run so role-editor denies apply.
84 $filtered = apply_filters( 'user_has_cap', $user->allcaps, array( $cap ), array( $cap, $user->ID ), $user );
85
86 return ! empty( $filtered[ $cap ] );
87 }
88 )
89 );
90 }
91
92 /**
93 * Get capabilities grouped by type.
94 *
95 * WooCommerce 9.9 replaced promote_users with create_customers for
96 * customer creation via the REST API. We show the correct capability
97 * on the Access settings page based on the installed WC version.
98 *
99 * The `wc` group lists PRIMITIVE capabilities only — the names a role grant
100 * can actually be read back from. `product` and `shop_coupon` register with
101 * map_meta_cap = true, so WordPress rewrites their singular meta caps
102 * (`edit_shop_coupon`, `delete_shop_coupon`, ...) into the `*_others_*` /
103 * `*_published_*` / `*_private_*` primitives below; exposing the singular
104 * names would be a dead toggle. `edit_product` / `delete_product` are the
105 * exception and ARE required: `product_variation` registers with
106 * capability_type `product` and map_meta_cap = false, so a variation write
107 * checks those two names literally.
108 *
109 * @return array
110 */
111 private static function get_caps(): array {
112 $customer_create_cap = version_compare( WC()->version, '9.9', '>=' )
113 ? 'create_customers'
114 : 'promote_users';
115
116 return array(
117 'wcpos' => array(
118 'access_woocommerce_pos',
119 'manage_woocommerce_pos',
120 ),
121 'wc' => array(
122 $customer_create_cap,
123 'read_private_products',
124 'edit_product',
125 'edit_products',
126 'edit_others_products',
127 'edit_private_products',
128 'edit_published_products',
129 'publish_products',
130 'delete_product',
131 'delete_products',
132 'delete_others_products',
133 'delete_private_products',
134 'delete_published_products',
135 'read_private_shop_orders',
136 'publish_shop_orders',
137 'edit_shop_orders',
138 'edit_others_shop_orders',
139 'edit_users',
140 'list_users',
141 'manage_product_terms',
142 'read_private_shop_coupons',
143 'edit_shop_coupons',
144 'edit_others_shop_coupons',
145 'edit_private_shop_coupons',
146 'edit_published_shop_coupons',
147 'publish_shop_coupons',
148 'delete_shop_coupons',
149 'delete_others_shop_coupons',
150 'delete_private_shop_coupons',
151 'delete_published_shop_coupons',
152 ),
153 'wp' => array(
154 'read',
155 ),
156 );
157 }
158
159 /**
160 * Read the section's public view: role capability groups computed from $wp_roles.
161 *
162 * @return array
163 */
164 public function read(): array {
165 global $wp_roles;
166 $role_caps = array();
167 $caps = self::get_caps();
168
169 $roles = $wp_roles->roles;
170 if ( $roles ) {
171 foreach ( $roles as $slug => $role ) {
172 $role_caps[ $slug ] = array(
173 'name' => $role['name'],
174 'capabilities' => array(
175 'wcpos' => array_intersect_key(
176 array_merge( array_fill_keys( $caps['wcpos'], false ), $role['capabilities'] ),
177 array_flip( $caps['wcpos'] )
178 ),
179 'wc' => array_intersect_key(
180 array_merge( array_fill_keys( $caps['wc'], false ), $role['capabilities'] ),
181 array_flip( $caps['wc'] )
182 ),
183 'wp' => array_intersect_key(
184 array_merge( array_fill_keys( $caps['wp'], false ), $role['capabilities'] ),
185 array_flip( $caps['wp'] )
186 ),
187 ),
188 );
189 }
190 }
191
192 /*
193 * Filters the access settings.
194 *
195 * @param {array} $settings
196 * @returns {array} $settings
197 * @since 1.0.0
198 * @hook woocommerce_pos_access_settings
199 */
200 return apply_filters( 'woocommerce_pos_access_settings', $role_caps );
201 }
202
203 /**
204 * Mutate role capabilities.
205 *
206 * Expects the payload to contain exactly one role slug key. The value is a
207 * partial structure with a 'capabilities' key whose groups (wcpos/wc/wp) map
208 * capability names to boolean grants. Only one role is mutated per call —
209 * this mirrors the single-role update semantics of the original REST
210 * controller.
211 *
212 * The administrator/read capability is never removed as a sanity guard.
213 *
214 * @param array $settings Incoming payload keyed by role slug.
215 *
216 * @return array|WP_Error The fresh read() view on success.
217 */
218 public function write( array $settings ) {
219 // Defense-in-depth: capability mutation is a privileged service-layer
220 // operation; do not rely solely on the REST route's permission
221 // callback (matches the Settings::delete_settings() precedent).
222 if ( ! current_user_can( 'edit_users' ) || ! current_user_can( 'promote_users' ) ) {
223 return new WP_Error(
224 'woocommerce_pos_settings_error',
225 __( 'You do not have permission to update access settings.', 'woocommerce-pos' ),
226 array( 'status' => 403 )
227 );
228 }
229
230 global $wp_roles;
231
232 // Intersect payload against known role slugs.
233 $roles = array_keys( $wp_roles->roles );
234 $update = array_intersect_key( $settings, array_flip( $roles ) );
235
236 // Only update a single role per call.
237 if ( 1 === \count( $update ) ) {
238 $slugs = array_keys( $update );
239 $slug = $slugs[0];
240 $role = get_role( $slug );
241
242 if ( $role ) {
243 // Build the allow-list of capabilities exposed for this role, including
244 // extension groups added via the woocommerce_pos_access_settings filter.
245 $access_settings = $this->read();
246 $allowed_caps = array();
247 if ( isset( $access_settings[ $slug ]['capabilities'] ) ) {
248 foreach ( $access_settings[ $slug ]['capabilities'] as $capabilities ) {
249 if ( \is_array( $capabilities ) ) {
250 $allowed_caps = array_merge( $allowed_caps, array_keys( $capabilities ) );
251 }
252 }
253 }
254
255 // Flatten capability groups (wcpos / wc / wp) into a single map.
256 $flattened_caps = array();
257 foreach ( $update[ $slug ]['capabilities'] as $capabilities ) {
258 if ( \is_array( $capabilities ) ) {
259 $flattened_caps = array_merge( $flattened_caps, $capabilities );
260 }
261 }
262
263 // Ignore capabilities outside the access settings view (issue #1159).
264 $flattened_caps = array_intersect_key( $flattened_caps, array_flip( $allowed_caps ) );
265 $flattened_caps = array_map( 'wp_validate_boolean', $flattened_caps );
266
267 // Apply each allowed capability grant/revoke.
268 foreach ( $flattened_caps as $cap => $grant ) {
269 // Sanity check: administrator role must always keep the `read` capability.
270 if ( 'administrator' === $slug && 'read' === $cap ) {
271 continue;
272 }
273 if ( $grant ) {
274 $role->add_cap( $cap );
275 } else {
276 $role->remove_cap( $cap );
277 }
278 }
279 }
280 }
281
282 return $this->read();
283 }
284
285 /**
286 * Full replacement, not a merge: the incoming payload IS the write.
287 *
288 * Writes mutate exactly one role per call and identify that role by the
289 * payload carrying a single known role slug. Merging the existing view in
290 * would hand write() every role on the site and silently turn a capability
291 * update into a no-op, so this section opts out of the default
292 * array_replace_recursive PATCH strategy.
293 *
294 * @param array $existing Existing settings view.
295 * @param array $patch Incoming payload keyed by role slug.
296 *
297 * @return array
298 */
299 public function merge( array $existing, array $patch ): array {
300 return $patch;
301 }
302
303 /**
304 * REST endpoint args — none required for access.
305 *
306 * @return array
307 */
308 public function endpoint_args(): array {
309 return array();
310 }
311 }
312