PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
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 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / API / V1 / Settings.php

Settings.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at includes/API/V1/Settings.php

421 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\API\V1;
9
10 use WCPOS\WooCommercePOS\Interfaces\Settings_Section_Interface;
11 use WCPOS\WooCommercePOS\Logger;
12 use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
13 use WCPOS\WooCommercePOS\Services\Tax_Id_Detector;
14 use WCPOS\WooCommercePOS\Services\Tax_Id_Settings;
15 use WCPOS\WooCommercePOS\Services\Tax_Id_Types;
16 use WP_Error;
17 use WP_REST_Controller;
18 use WP_REST_Request;
19 use WP_REST_Response;
20 use WP_REST_Server;
21
22 use const WCPOS\WooCommercePOS\SHORT_NAME;
23
24 /**
25 * Class Settings REST API.
26 *
27 * The per-section routes are projected from the Section Registry rather than
28 * hand-written: every registered Settings Section gets a GET/POST pair at
29 * /settings/{slug} whose args come from the section's endpoint_args(), whose
30 * reads call read(), and whose writes call write( merge( read(), $patch ) ).
31 * Registering a section through the
32 * `woocommerce_pos_register_settings_sections` action is therefore all an
33 * extension needs to do to gain an HTTP surface.
34 */
35 class Settings extends WP_REST_Controller {
36 /**
37 * Sections whose read route uses a permission callback other than the
38 * default manage_woocommerce_pos check.
39 *
40 * Frozen legacy parity, not policy: POS clients need to read server-owned
41 * Cloud Printer targets configured by a manager, so cloud_print reads only
42 * require access_woocommerce_pos.
43 *
44 * @var array<string, string>
45 */
46 private const READ_PERMISSION_CALLBACKS = array(
47 'cloud_print' => 'cloud_print_read_permission_check',
48 );
49
50 /**
51 * Sections whose update route uses a permission callback other than the
52 * default manage_woocommerce_pos check.
53 *
54 * The access section mutates WordPress role capabilities, so its writes
55 * require edit_users + promote_users.
56 *
57 * @var array<string, string>
58 */
59 private const UPDATE_PERMISSION_CALLBACKS = array(
60 'access' => 'update_access_permission_check',
61 );
62
63 /**
64 * Route slugs that are not the dashed form of the section id. Published
65 * URLs are frozen public interface — tax_ids shipped with an underscore.
66 *
67 * @var array<string, string>
68 */
69 private const LEGACY_ROUTE_SLUGS = array(
70 'tax_ids' => 'tax_ids',
71 );
72
73 /**
74 * Sections whose update route answers a write failure with a flat
75 * { code, message } body at 400 instead of the WP_Error envelope. Frozen
76 * wire contract — cloud-print clients do not expect the extra data key.
77 *
78 * @var string[]
79 */
80 private const FLAT_ERROR_SECTIONS = array( 'cloud_print' );
81
82 /**
83 * Endpoint namespace.
84 *
85 * @var string
86 */
87 protected $namespace = SHORT_NAME . '/v1';
88
89 /**
90 * Route base.
91 *
92 * @var string
93 */
94 protected $rest_base = 'settings';
95
96 /**
97 * Settings constructor.
98 */
99 public function __construct() {
100 add_filter( 'option_woocommerce_pos_settings_payment_gateways', array( $this, 'payment_gateways_settings' ) );
101 }
102
103 /**
104 * Register routes.
105 *
106 * @return void
107 */
108 public function register_routes(): void {
109 $route_slugs = array();
110 register_rest_route(
111 $this->namespace,
112 '/' . $this->rest_base,
113 array(
114 'methods' => WP_REST_Server::READABLE,
115 'callback' => array( $this, 'get_items' ),
116 'permission_callback' => '__return_true',
117 )
118 );
119
120 foreach ( SettingsService::instance()->sections()->all() as $id => $section ) {
121 $id = (string) $id;
122
123 // A section id becomes part of a route regex, so anything outside the
124 // documented id alphabet is refused rather than compiled into the
125 // route table.
126 if ( ! preg_match( '/^[a-z0-9_-]+$/', $id ) ) {
127 Logger::warning(
128 \sprintf(
129 'Settings section "%s" has no REST route: section ids must match [a-z0-9_-].',
130 $id
131 )
132 );
133
134 continue;
135 }
136
137 if ( isset( $route_slugs[ $this->route_slug( $id ) ] ) ) {
138 Logger::warning( sprintf( 'Settings section "%s" has no REST route: route slug already registered.', $id ) );
139 continue;
140 }
141 $route_slugs[ $this->route_slug( $id ) ] = true;
142 $this->register_section_routes( $id, $section );
143 }
144
145 // Section-adjacent read-only lookups. These are not section CRUD, so they
146 // stay hand-registered.
147 register_rest_route(
148 $this->namespace,
149 '/' . $this->rest_base . '/tax_ids/detection',
150 array(
151 'methods' => WP_REST_Server::READABLE,
152 'callback' => array( $this, 'get_tax_ids_detection' ),
153 'permission_callback' => array( $this, 'read_permission_check' ),
154 )
155 );
156 }
157
158 /**
159 * The route slug for a section id.
160 *
161 * @param string $id Section id.
162 *
163 * @return string
164 */
165 public function route_slug( string $id ): string {
166 return self::LEGACY_ROUTE_SLUGS[ $id ] ?? str_replace( '_', '-', $id );
167 }
168
169 /**
170 * Read a section's public view.
171 *
172 * @param string $id Section id.
173 * @param WP_REST_Request $request Full details about the request.
174 *
175 * @return WP_Error|WP_REST_Response
176 */
177 public function get_section_settings( string $id, WP_REST_Request $request ) {
178 $section = SettingsService::instance()->sections()->get( $id );
179
180 if ( ! $section instanceof Settings_Section_Interface ) {
181 return $this->section_not_registered_error();
182 }
183
184 return new WP_REST_Response( $section->read(), 200 );
185 }
186
187 /**
188 * Update a section.
189 *
190 * POST data is treated as PATCH, ie: partial, so it is merged over the
191 * existing view before the section persists it. Sections whose payload is a
192 * full replacement (access, cloud_print) say so by overriding merge().
193 *
194 * @param string $id Section id.
195 * @param WP_REST_Request $request Full details about the request.
196 *
197 * @return array|WP_Error|WP_REST_Response
198 */
199 public function update_section_settings( string $id, WP_REST_Request $request ) {
200 $section = SettingsService::instance()->sections()->get( $id );
201
202 if ( ! $section instanceof Settings_Section_Interface ) {
203 return $this->section_not_registered_error();
204 }
205
206 $payload = $request->get_json_params();
207 if ( empty( $payload ) ) {
208 $payload = $request->get_body_params();
209 }
210
211 $result = $section->write( $section->merge( $section->read(), (array) $payload ) );
212
213 if ( is_wp_error( $result ) && \in_array( $id, self::FLAT_ERROR_SECTIONS, true ) ) {
214 // Keep the historical error body shape {code, message} — clients do
215 // not expect WP_Error's extra data envelope here.
216 return new WP_REST_Response(
217 array(
218 'code' => $result->get_error_code(),
219 'message' => $result->get_error_message(),
220 ),
221 400
222 );
223 }
224
225 return $result;
226 }
227
228 /**
229 * Get tax-ID auto-detection summary for the Compatibility tab.
230 *
231 * Returns the active third-party plugin ids, the per-type defaults, and the
232 * fully composed write_map (defaults < inferred < plugin claims < user
233 * overrides). The UI renders the composed map and surfaces overrides
234 * inline.
235 *
236 * @param WP_REST_Request $request Full details about the request.
237 *
238 * @return WP_REST_Response
239 */
240 public function get_tax_ids_detection( WP_REST_Request $request ) {
241 $summary = ( new Tax_Id_Detector() )->summary();
242
243 $response = new WP_REST_Response(
244 array(
245 'plugins' => $summary['plugins'],
246 'default_write_map' => Tax_Id_Settings::default_write_map(),
247 'composed_write_map' => $summary['write_map'],
248 // Only customer-applicable types are surfaced: business-register
249 // identifiers (DE/NL/FR/CH commercial-register types) live on the
250 // store, not on customers, so they have no write-map row.
251 'types' => Tax_Id_Types::customer_applicable_types(),
252 )
253 );
254 $response->set_status( 200 );
255
256 return $response;
257 }
258
259 /**
260 * Sanitize a cloud assignment entry.
261 *
262 * Kept for backward compatibility — the Settings_CloudPrint_Test conformance
263 * gate exercises this method directly via ReflectionMethod. Delegates to
264 * Cloud_Print_Section::sanitize_assignment() when the section is registered.
265 *
266 * @param mixed $assignment Assignment.
267 *
268 * @return array
269 *
270 * @phpstan-ignore-next-line
271 */
272 private function sanitize_cloud_assignment( $assignment ): array {
273 $section = SettingsService::instance()->sections()->get( 'cloud_print' );
274
275 if ( $section instanceof \WCPOS\WooCommercePOS\Services\Settings\Cloud_Print_Section ) {
276 return $section->sanitize_assignment( $assignment );
277 }
278
279 $assignment = \is_array( $assignment ) ? $assignment : array();
280 $assignment['copies'] = min( 5, max( 1, (int) ( $assignment['copies'] ?? 1 ) ) );
281
282 return $assignment;
283 }
284
285 /**
286 * Check read permissions for a section.
287 *
288 * @param string $id Section id.
289 *
290 * @return bool
291 */
292 public function section_read_permission_check( string $id ): bool {
293 $callback = self::READ_PERMISSION_CALLBACKS[ $id ] ?? 'read_permission_check';
294
295 return (bool) \call_user_func( array( $this, $callback ) );
296 }
297
298 /**
299 * Check update permissions for a section.
300 *
301 * @param string $id Section id.
302 *
303 * @return bool
304 */
305 public function section_update_permission_check( string $id ): bool {
306 $callback = self::UPDATE_PERMISSION_CALLBACKS[ $id ] ?? 'update_permission_check';
307
308 return (bool) \call_user_func( array( $this, $callback ) );
309 }
310
311 /**
312 * Check read permissions.
313 *
314 * @TODO - who can read settings?
315 *
316 * @return bool
317 */
318 public function read_permission_check(): bool {
319 return current_user_can( 'manage_woocommerce_pos' );
320 }
321
322 /**
323 * Check Cloud Print read permissions.
324 *
325 * POS clients need to read server-owned Cloud Printer targets so they can route
326 * receipts to printers configured by a manager. Updating the server-owned
327 * settings still requires manage_woocommerce_pos via update_permission_check().
328 *
329 * @return bool
330 */
331 public function cloud_print_read_permission_check(): bool {
332 return current_user_can( 'access_woocommerce_pos' );
333 }
334
335 /**
336 * Check update permissions.
337 *
338 * @return bool
339 */
340 public function update_permission_check(): bool {
341 return current_user_can( 'manage_woocommerce_pos' );
342 }
343
344 /**
345 * Check access update permissions.
346 *
347 * @return bool
348 */
349 public function update_access_permission_check(): bool {
350 return current_user_can( 'edit_users' ) && current_user_can( 'promote_users' );
351 }
352
353 /**
354 * Filter payment gateways settings.
355 *
356 * @param mixed $options The gateway options.
357 */
358 public function payment_gateways_settings( $options ) {
359 foreach ( $options['gateways'] as $gateway_id => &$gateway_data ) {
360 if ( ! \in_array( $gateway_id, array( 'pos_cash', 'pos_card' ), true ) ) {
361 $gateway_data['enabled'] = false;
362 }
363 }
364 if ( ! \in_array( $options['default_gateway'], array( 'pos_cash', 'pos_card' ), true ) ) {
365 $options['default_gateway'] = 'pos_cash';
366 }
367
368 return $options;
369 }
370
371
372 /**
373 * Register the GET/POST pair for one Settings Section.
374 *
375 * @param string $id Section id.
376 * @param Settings_Section_Interface $section The section.
377 *
378 * @return void
379 */
380 private function register_section_routes( string $id, Settings_Section_Interface $section ): void {
381 register_rest_route(
382 $this->namespace,
383 '/' . $this->rest_base . '/' . $this->route_slug( $id ),
384 array(
385 array(
386 'methods' => WP_REST_Server::READABLE,
387 'callback' => function ( WP_REST_Request $request ) use ( $id ) {
388 return $this->get_section_settings( $id, $request );
389 },
390 'permission_callback' => function () use ( $id ) {
391 return $this->section_read_permission_check( $id );
392 },
393 ),
394 array(
395 'methods' => WP_REST_Server::EDITABLE,
396 'callback' => function ( WP_REST_Request $request ) use ( $id ) {
397 return $this->update_section_settings( $id, $request );
398 },
399 'permission_callback' => function () use ( $id ) {
400 return $this->section_update_permission_check( $id );
401 },
402 'args' => $section->endpoint_args(),
403 ),
404 )
405 );
406 }
407
408 /**
409 * The error returned when a route outlives its section.
410 *
411 * @return WP_Error
412 */
413 private function section_not_registered_error(): WP_Error {
414 return new WP_Error(
415 'woocommerce_pos_settings_error',
416 __( 'Settings section not registered.', 'woocommerce-pos' ),
417 array( 'status' => 500 )
418 );
419 }
420 }
421