PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.15
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 / API / Settings.php

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

1,101 lines 33.0 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;
9
10 use Closure;
11 use WCPOS\WooCommercePOS\Services\Cloud_Print_Registry;
12 use WCPOS\WooCommercePOS\Services\Cloud_Print_Relay_Service;
13 use WCPOS\WooCommercePOS\Services\Provider;
14 use WCPOS\WooCommercePOS\Services\Settings as SettingsService;
15 use WCPOS\WooCommercePOS\Services\Tax_Id_Detector;
16 use WCPOS\WooCommercePOS\Services\Tax_Id_Settings;
17 use WCPOS\WooCommercePOS\Services\Tax_Id_Types;
18 use WP_Error;
19 use WP_REST_Controller;
20 use WP_REST_Request;
21 use WP_REST_Response;
22 use WP_REST_Server;
23
24 use const WCPOS\WooCommercePOS\SHORT_NAME;
25
26 /**
27 * Class Settings REST API.
28 */
29 class Settings extends WP_REST_Controller {
30 /**
31 * Endpoint namespace.
32 *
33 * @var string
34 */
35 protected $namespace = SHORT_NAME . '/v1';
36
37 /**
38 * Route base.
39 *
40 * @var string
41 */
42 protected $rest_base = 'settings';
43
44 /**
45 * Settings constructor.
46 */
47 public function __construct() {
48 add_filter( 'option_woocommerce_pos_settings_payment_gateways', array( $this, 'payment_gateways_settings' ) );
49
50 // remove this once Pro settings have been moved to the new settings service.
51 add_filter( 'pre_update_option_woocommerce_pos_pro_settings_license', array( $this, 'remove_license_transient' ) );
52 }
53
54 /**
55 * Register routes.
56 *
57 * @return void
58 */
59 public function register_routes(): void {
60 register_rest_route(
61 $this->namespace,
62 '/' . $this->rest_base,
63 array(
64 'methods' => WP_REST_Server::READABLE,
65 'callback' => array( $this, 'get_items' ),
66 'permission_callback' => '__return_true',
67 )
68 );
69
70 register_rest_route(
71 $this->namespace,
72 '/' . $this->rest_base . '/general',
73 array(
74 'methods' => WP_REST_Server::READABLE,
75 'callback' => array( $this, 'get_general_settings' ),
76 'permission_callback' => array( $this, 'read_permission_check' ),
77 )
78 );
79
80 // register_rest_route(
81 // $this->namespace,
82 // '/' . $this->rest_base . '/general/barcodes',
83 // array(
84 // 'methods' => WP_REST_Server::READABLE,
85 // 'callback' => array( $this, 'get_barcodes' ),
86 // 'permission_callback' => array( $this, 'read_permission_check' ),
87 // )
88 // );.
89
90 register_rest_route(
91 $this->namespace,
92 '/' . $this->rest_base . '/general',
93 array(
94 'methods' => WP_REST_Server::EDITABLE,
95 'callback' => array( $this, 'update_general_settings' ),
96 'permission_callback' => array( $this, 'update_permission_check' ),
97 'args' => $this->get_general_endpoint_args(),
98 )
99 );
100
101 register_rest_route(
102 $this->namespace,
103 '/' . $this->rest_base . '/checkout',
104 array(
105 'methods' => WP_REST_Server::READABLE,
106 'callback' => array( $this, 'get_checkout_settings' ),
107 'permission_callback' => array( $this, 'read_permission_check' ),
108 )
109 );
110
111 register_rest_route(
112 $this->namespace,
113 '/' . $this->rest_base . '/checkout/order-statuses',
114 array(
115 'methods' => WP_REST_Server::READABLE,
116 'callback' => array( $this, 'get_order_statuses' ),
117 'permission_callback' => array( $this, 'read_permission_check' ),
118 )
119 );
120
121 register_rest_route(
122 $this->namespace,
123 '/' . $this->rest_base . '/checkout',
124 array(
125 'methods' => WP_REST_Server::EDITABLE,
126 'callback' => array( $this, 'update_checkout_settings' ),
127 'permission_callback' => array( $this, 'update_permission_check' ),
128 'args' => $this->get_checkout_endpoint_args(),
129 )
130 );
131
132 register_rest_route(
133 $this->namespace,
134 '/' . $this->rest_base . '/payment-gateways',
135 array(
136 'methods' => WP_REST_Server::READABLE,
137 'callback' => array( $this, 'get_payment_gateways_settings' ),
138 'permission_callback' => array( $this, 'read_permission_check' ),
139 )
140 );
141
142 register_rest_route(
143 $this->namespace,
144 '/' . $this->rest_base . '/payment-gateways',
145 array(
146 'methods' => WP_REST_Server::EDITABLE,
147 'callback' => array( $this, 'update_payment_gateways_settings' ),
148 'permission_callback' => array( $this, 'update_permission_check' ),
149 'args' => $this->get_checkout_endpoint_args(),
150 )
151 );
152
153 register_rest_route(
154 $this->namespace,
155 '/' . $this->rest_base . '/tax_ids',
156 array(
157 'methods' => WP_REST_Server::READABLE,
158 'callback' => array( $this, 'get_tax_ids_settings' ),
159 'permission_callback' => array( $this, 'read_permission_check' ),
160 )
161 );
162
163 register_rest_route(
164 $this->namespace,
165 '/' . $this->rest_base . '/tax_ids',
166 array(
167 'methods' => WP_REST_Server::EDITABLE,
168 'callback' => array( $this, 'update_tax_ids_settings' ),
169 'permission_callback' => array( $this, 'update_permission_check' ),
170 'args' => $this->get_tax_ids_endpoint_args(),
171 )
172 );
173
174 register_rest_route(
175 $this->namespace,
176 '/' . $this->rest_base . '/tax_ids/detection',
177 array(
178 'methods' => WP_REST_Server::READABLE,
179 'callback' => array( $this, 'get_tax_ids_detection' ),
180 'permission_callback' => array( $this, 'read_permission_check' ),
181 )
182 );
183
184 register_rest_route(
185 $this->namespace,
186 '/' . $this->rest_base . '/access',
187 array(
188 'methods' => WP_REST_Server::READABLE,
189 'callback' => array( $this, 'get_access_settings' ),
190 'permission_callback' => array( $this, 'read_permission_check' ),
191 )
192 );
193
194 register_rest_route(
195 $this->namespace,
196 '/' . $this->rest_base . '/access',
197 array(
198 'methods' => WP_REST_Server::EDITABLE,
199 'callback' => array( $this, 'update_access_settings' ),
200 'permission_callback' => array( $this, 'update_access_permission_check' ),
201 )
202 );
203
204 register_rest_route(
205 $this->namespace,
206 '/' . $this->rest_base . '/tools',
207 array(
208 'methods' => WP_REST_Server::READABLE,
209 'callback' => array( $this, 'get_tools_settings' ),
210 'permission_callback' => array( $this, 'read_permission_check' ),
211 )
212 );
213
214 register_rest_route(
215 $this->namespace,
216 '/' . $this->rest_base . '/tools',
217 array(
218 'methods' => WP_REST_Server::EDITABLE,
219 'callback' => array( $this, 'update_tools_settings' ),
220 'permission_callback' => array( $this, 'update_permission_check' ),
221 )
222 );
223
224 register_rest_route(
225 $this->namespace,
226 '/' . $this->rest_base . '/license',
227 array(
228 'methods' => WP_REST_Server::READABLE,
229 'callback' => array( $this, 'get_license_settings' ),
230 'permission_callback' => array( $this, 'read_permission_check' ),
231 )
232 );
233
234 register_rest_route(
235 $this->namespace,
236 '/' . $this->rest_base . '/cloud-print',
237 array(
238 array(
239 'methods' => WP_REST_Server::READABLE,
240 'callback' => array( $this, 'get_cloud_print_settings' ),
241 'permission_callback' => array( $this, 'cloud_print_read_permission_check' ),
242 ),
243 array(
244 'methods' => WP_REST_Server::EDITABLE,
245 'callback' => array( $this, 'update_cloud_print_settings' ),
246 'permission_callback' => array( $this, 'update_permission_check' ),
247 ),
248 )
249 );
250 }
251
252 /**
253 * Get general endpoint arguments.
254 *
255 * @return Closure[][]
256 */
257 public function get_general_endpoint_args(): array {
258 return array(
259 'pos_only_products' => array(
260 'validate_callback' => function ( $param, $request, $key ) {
261 return \is_bool( $param );
262 },
263 ),
264 'decimal_qty' => array(
265 'validate_callback' => function ( $param, $request, $key ) {
266 return \is_bool( $param );
267 },
268 ),
269 'force_ssl' => array(
270 'validate_callback' => function ( $param, $request, $key ) {
271 return \is_bool( $param );
272 },
273 ),
274 'default_customer' => array(
275 'validate_callback' => function ( $param, $request, $key ) {
276 return \is_integer( $param );
277 },
278 ),
279 'default_customer_is_cashier' => array(
280 'validate_callback' => function ( $param, $request, $key ) {
281 return \is_bool( $param );
282 },
283 ),
284 'barcode_field' => array(
285 'validate_callback' => function ( $param, $request, $key ) {
286 return \is_string( $param );
287 },
288 ),
289 'generate_username' => array(
290 'validate_callback' => function ( $param, $request, $key ) {
291 return \is_bool( $param );
292 },
293 ),
294 'restore_stock_on_delete' => array(
295 'validate_callback' => function ( $param, $request, $key ) {
296 return \is_bool( $param );
297 },
298 ),
299 'storefront_receipt_enabled' => array(
300 'validate_callback' => function ( $param, $request, $key ) {
301 return \is_bool( $param );
302 },
303 ),
304 'storefront_receipt_template' => array(
305 'validate_callback' => function ( $param, $request, $key ) {
306 return \is_string( $param );
307 },
308 ),
309 'tracking_consent' => array(
310 'validate_callback' => function ( $param, $request, $key ) {
311 return \is_string( $param ) && \in_array( $param, array( 'allowed', 'denied', 'undecided' ), true );
312 },
313 ),
314 'store_tax_ids' => array(
315 'validate_callback' => function ( $param, $request, $key ) {
316 return \is_array( $param );
317 },
318 ),
319 'store_name' => array(
320 'validate_callback' => function ( $param, $request, $key ) {
321 return \is_string( $param );
322 },
323 ),
324 'store_phone' => array(
325 'validate_callback' => function ( $param, $request, $key ) {
326 return \is_string( $param );
327 },
328 ),
329 'store_email' => array(
330 'validate_callback' => function ( $param, $request, $key ) {
331 return \is_string( $param );
332 },
333 ),
334 'policies_and_conditions' => array(
335 'validate_callback' => function ( $param, $request, $key ) {
336 return \is_string( $param );
337 },
338 ),
339 );
340 }
341
342 /**
343 * Get tax IDs endpoint arguments.
344 *
345 * @return Closure[][]
346 */
347 public function get_tax_ids_endpoint_args(): array {
348 return array(
349 'write_map' => array(
350 'validate_callback' => function ( $param, $request, $key ) {
351 if ( ! \is_array( $param ) ) {
352 return false;
353 }
354 foreach ( $param as $type => $meta_key ) {
355 if ( ! \is_string( $type ) || ! Tax_Id_Types::is_valid_type( $type ) ) {
356 return false;
357 }
358 if ( ! \is_string( $meta_key ) ) {
359 return false;
360 }
361 }
362
363 return true;
364 },
365 ),
366 );
367 }
368
369 /**
370 * Get checkout endpoint arguments.
371 *
372 * @return Closure[][]
373 */
374 public function get_checkout_endpoint_args(): array {
375 return array(
376 'receipt_default_mode' => array(
377 'validate_callback' => function ( $param, $request, $key ) {
378 return \is_string( $param ) && \in_array( $param, array( 'fiscal', 'live' ), true );
379 },
380 ),
381 'admin_emails' => array(
382 'validate_callback' => function ( $param, $request, $key ) {
383 return \is_array( $param );
384 },
385 ),
386 'customer_emails' => array(
387 'validate_callback' => function ( $param, $request, $key ) {
388 return \is_array( $param );
389 },
390 ),
391 'cashier_emails' => array(
392 'validate_callback' => function ( $param, $request, $key ) {
393 return \is_array( $param );
394 },
395 ),
396 'auto_print_receipt' => array(
397 'validate_callback' => function ( $param, $request, $key ) {
398 return \is_bool( $param );
399 },
400 ),
401 'default_gateway' => array(
402 'validate_callback' => function ( $param, $request, $key ) {
403 return \is_string( $param );
404 },
405 ),
406 'gateways' => array(
407 'validate_callback' => function ( $param, $request, $key ) {
408 return \is_array( $param );
409 },
410 ),
411 );
412 }
413
414 /**
415 * Get general settings.
416 *
417 * @param WP_REST_Request $request Full details about the request.
418 *
419 * @return array|WP_Error|WP_REST_Response
420 */
421 public function get_general_settings( WP_REST_Request $request ) {
422 $general_settings = woocommerce_pos_get_settings( 'general' );
423
424 if ( is_wp_error( $general_settings ) ) {
425 return $general_settings;
426 }
427
428 // Create the response object.
429 $response = new WP_REST_Response( $general_settings );
430
431 // Set the status code of the response.
432 $response->set_status( 200 );
433
434 return $response;
435 }
436
437 /**
438 * Get checkout settings.
439 *
440 * @param WP_REST_Request $request Full details about the request.
441 *
442 * @return array|WP_Error|WP_REST_Response
443 */
444 public function get_checkout_settings( WP_REST_Request $request ) {
445 $checkout_settings = woocommerce_pos_get_settings( 'checkout' );
446
447 if ( is_wp_error( $checkout_settings ) ) {
448 return $checkout_settings;
449 }
450
451 // Create the response object.
452 $response = new WP_REST_Response( $checkout_settings );
453
454 // Set the status code of the response.
455 $response->set_status( 200 );
456
457 return $response;
458 }
459
460 /**
461 * Get payment gateways settings.
462 *
463 * @param WP_REST_Request $request Full details about the request.
464 *
465 * @return array|WP_Error|WP_REST_Response
466 */
467 public function get_payment_gateways_settings( WP_REST_Request $request ) {
468 $payment_gateways_settings = woocommerce_pos_get_settings( 'payment_gateways' );
469
470 if ( is_wp_error( $payment_gateways_settings ) ) {
471 return $payment_gateways_settings;
472 }
473
474 // Create the response object.
475 $response = new WP_REST_Response( $payment_gateways_settings );
476
477 // Set the status code of the response.
478 $response->set_status( 200 );
479
480 return $response;
481 }
482
483 /**
484 * Get tax IDs settings.
485 *
486 * @param WP_REST_Request $request Full details about the request.
487 *
488 * @return array|WP_Error|WP_REST_Response
489 */
490 public function get_tax_ids_settings( WP_REST_Request $request ) {
491 $tax_ids_settings = woocommerce_pos_get_settings( 'tax_ids' );
492
493 if ( is_wp_error( $tax_ids_settings ) ) {
494 return $tax_ids_settings;
495 }
496
497 $response = new WP_REST_Response( $tax_ids_settings );
498 $response->set_status( 200 );
499
500 return $response;
501 }
502
503 /**
504 * Update tax IDs settings. POST data is treated as PATCH (partial), merged
505 * with existing settings.
506 *
507 * @param WP_REST_Request $request Full details about the request.
508 *
509 * @return array|WP_Error
510 */
511 public function update_tax_ids_settings( WP_REST_Request $request ) {
512 $old_settings = woocommerce_pos_get_settings( 'tax_ids' );
513 $payload = $request->get_json_params();
514
515 // `write_map` is intentionally a full replacement (not deep-merged) so
516 // users can remove entries by sending the trimmed map.
517 $settings = array_replace_recursive( $old_settings, $payload );
518 if ( isset( $payload['write_map'] ) && \is_array( $payload['write_map'] ) ) {
519 $settings['write_map'] = $payload['write_map'];
520 }
521
522 $settings_service = SettingsService::instance();
523
524 return $settings_service->save_settings( 'tax_ids', $settings );
525 }
526
527 /**
528 * Get tax-ID auto-detection summary for the Compatibility tab.
529 *
530 * Returns the active third-party plugin ids, the per-type defaults, and the
531 * fully composed write_map (defaults < inferred < plugin claims < user
532 * overrides). The UI renders the composed map and surfaces overrides
533 * inline.
534 *
535 * @param WP_REST_Request $request Full details about the request.
536 *
537 * @return WP_REST_Response
538 */
539 public function get_tax_ids_detection( WP_REST_Request $request ) {
540 $summary = ( new Tax_Id_Detector() )->summary();
541
542 $response = new WP_REST_Response(
543 array(
544 'plugins' => $summary['plugins'],
545 'default_write_map' => Tax_Id_Settings::default_write_map(),
546 'composed_write_map' => $summary['write_map'],
547 // Only customer-applicable types are surfaced: business-register
548 // identifiers (DE/NL/FR/CH commercial-register types) live on the
549 // store, not on customers, so they have no write-map row.
550 'types' => Tax_Id_Types::customer_applicable_types(),
551 )
552 );
553 $response->set_status( 200 );
554
555 return $response;
556 }
557
558 /**
559 * Get access settings.
560 *
561 * @param WP_REST_Request $request Full details about the request.
562 *
563 * @return array|WP_Error|WP_REST_Response
564 */
565 public function get_access_settings( WP_REST_Request $request ) {
566 $access_settings = woocommerce_pos_get_settings( 'access' );
567
568 if ( is_wp_error( $access_settings ) ) {
569 return $access_settings;
570 }
571
572 // Create the response object.
573 $response = new WP_REST_Response( $access_settings );
574
575 // Set the status code of the response.
576 $response->set_status( 200 );
577
578 return $response;
579 }
580
581 /**
582 * Get tools settings.
583 *
584 * @param WP_REST_Request $request Full details about the request.
585 *
586 * @return array|WP_Error|WP_REST_Response
587 */
588 public function get_tools_settings( WP_REST_Request $request ) {
589 $tools_settings = woocommerce_pos_get_settings( 'tools' );
590
591 if ( is_wp_error( $tools_settings ) ) {
592 return $tools_settings;
593 }
594
595 // Create the response object.
596 $response = new WP_REST_Response( $tools_settings );
597
598 // Set the status code of the response.
599 $response->set_status( 200 );
600
601 return $response;
602 }
603
604 /**
605 * Get license settings.
606 *
607 * @param WP_REST_Request $request Full details about the request.
608 *
609 * @return array|WP_Error|WP_REST_Response
610 */
611 public function get_license_settings( WP_REST_Request $request ) {
612 $license_settings = woocommerce_pos_get_settings( 'license' );
613
614 if ( is_wp_error( $license_settings ) ) {
615 return $license_settings;
616 }
617
618 // Create the response object.
619 $response = new WP_REST_Response( $license_settings );
620
621 // Set the status code of the response.
622 $response->set_status( 200 );
623
624 return $response;
625 }
626
627 /**
628 * Get cloud-print settings.
629 *
630 * @return WP_REST_Response
631 */
632 public function get_cloud_print_settings() {
633 $settings = get_option( 'woocommerce_pos_settings_cloud_print', array() );
634 $settings = wp_parse_args(
635 \is_array( $settings ) ? $settings : array(),
636 array(
637 'printers' => array(),
638 'assignments' => array(),
639 )
640 );
641 $registry = new Cloud_Print_Registry();
642 $settings['printers'] = array_map(
643 function ( $printer ) use ( $registry ) {
644 if ( ! \is_array( $printer ) ) {
645 return $printer;
646 }
647 $id = (string) ( $printer['id'] ?? '' );
648 $seen = $registry->get_seen( $id );
649 $printer = $this->with_cloud_printer_encoding_fields( $printer );
650 $printer['status'] = $registry->status_for( $id );
651 $printer['last_seen'] = $seen > 0 ? $seen : null;
652 if ( 'blocked' === $printer['status'] ) {
653 $printer['status_detail'] = $registry->status_detail_for( $id );
654 } else {
655 unset( $printer['status_detail'] );
656 }
657 unset( $printer['poll_token_hash'], $printer['printnode_api_key'], $printer['star_api_key'] );
658
659 return $printer;
660 },
661 $settings['printers']
662 );
663 $settings['relay'] = Cloud_Print_Relay_Service::public_state();
664
665 return new WP_REST_Response( $settings, 200 );
666 }
667
668 /**
669 * Replace cloud-print settings.
670 *
671 * @param WP_REST_Request $request Request.
672 *
673 * @return WP_REST_Response
674 */
675 public function update_cloud_print_settings( WP_REST_Request $request ) {
676 $payload = $request->get_json_params();
677 if ( empty( $payload ) ) {
678 $payload = $request->get_body_params();
679 }
680 $printers = isset( $payload['printers'] ) && \is_array( $payload['printers'] ) ? array_values( $payload['printers'] ) : array();
681 $assigns = isset( $payload['assignments'] ) && \is_array( $payload['assignments'] ) ? array_values( $payload['assignments'] ) : array();
682
683 $existing = get_option( 'woocommerce_pos_settings_cloud_print', array() );
684 $existing_hashes = array();
685 $existing_keys = array();
686 $existing_star_keys = array();
687 $existing_ids = array();
688 if ( isset( $existing['printers'] ) && \is_array( $existing['printers'] ) ) {
689 foreach ( $existing['printers'] as $printer ) {
690 if ( ! empty( $printer['id'] ) ) {
691 $existing_ids[] = $printer['id'];
692 }
693 if ( ! empty( $printer['id'] ) && ! empty( $printer['poll_token_hash'] ) ) {
694 $existing_hashes[ $printer['id'] ] = $printer['poll_token_hash'];
695 }
696 if ( ! empty( $printer['id'] ) && ! empty( $printer['printnode_api_key'] ) ) {
697 $existing_keys[ $printer['id'] ] = $printer['printnode_api_key'];
698 }
699 if ( ! empty( $printer['id'] ) && ! empty( $printer['star_api_key'] ) ) {
700 $existing_star_keys[ $printer['id'] ] = $printer['star_api_key'];
701 }
702 }
703 }
704
705 $generated = array();
706 $clean_printers = array();
707 $seen_ids = array();
708 foreach ( $printers as $printer ) {
709 $printer = $this->sanitize_cloud_printer( $printer );
710
711 if ( '' === $printer['id'] ) {
712 $printer['id'] = Cloud_Print_Registry::derive_id( $printer['name'], array_merge( $existing_ids, array_keys( $seen_ids ) ) );
713 }
714 $id = $printer['id'];
715
716 // Preserve a previously stored PrintNode API key when the incoming
717 // payload omits it (GET strips the key, so the React app re-POSTs
718 // printers without it when toggling other fields). A non-empty
719 // incoming key still overwrites, letting users rotate it.
720 if ( 'printnode' === $printer['provider'] && '' === $printer['printnode_api_key'] && ! empty( $existing_keys[ $id ] ) ) {
721 $printer['printnode_api_key'] = $existing_keys[ $id ];
722 }
723
724 if ( 'star-online' === $printer['provider'] && '' === $printer['star_api_key'] && ! empty( $existing_star_keys[ $id ] ) ) {
725 $printer['star_api_key'] = $existing_star_keys[ $id ];
726 }
727
728 if ( 'star-online' === $printer['provider'] ) {
729 $api_base = \WCPOS\WooCommercePOS\Services\Star_Online_Client::api_base_from_cloudprnt_url( (string) $printer['star_cloudprnt_url'] );
730 $group = \WCPOS\WooCommercePOS\Services\Star_Online_Client::group_from_cloudprnt_url( (string) $printer['star_cloudprnt_url'] );
731 if ( '' === $printer['star_api_key'] || null === $api_base || '' === $group || '' === $printer['star_device_id'] ) {
732 return new WP_REST_Response(
733 array(
734 'code' => 'wcpos_cloud_print_star_online_invalid',
735 'message' => __( 'Star Online printers need an API key, a valid stario.online CloudPRNT URL, and a device.', 'woocommerce-pos' ),
736 ),
737 400
738 );
739 }
740 }
741
742 if ( isset( $seen_ids[ $id ] ) ) {
743 return new WP_REST_Response(
744 array(
745 'code' => 'wcpos_cloud_print_duplicate_printer_id',
746 'message' => __( 'Duplicate printer id.', 'woocommerce-pos' ),
747 ),
748 400
749 );
750 }
751 $seen_ids[ $id ] = true;
752
753 $regenerate = ! empty( $printer['regenerate_token'] );
754 unset( $printer['regenerate_token'] );
755
756 if ( Provider::is_polling( $printer['provider'] ) ) {
757 if ( $regenerate || empty( $existing_hashes[ $id ] ) ) {
758 $token = Cloud_Print_Registry::generate_token();
759 $printer['poll_token_hash'] = Cloud_Print_Registry::hash_token( $token );
760 $generated[ $id ] = $token;
761 } else {
762 $printer['poll_token_hash'] = $existing_hashes[ $id ];
763 }
764 }
765
766 $clean_printers[] = $printer;
767 }
768
769 $clean = array(
770 'printers' => $clean_printers,
771 'assignments' => array_map( array( $this, 'sanitize_cloud_assignment' ), $assigns ),
772 );
773 update_option( 'woocommerce_pos_settings_cloud_print', $clean );
774
775 // Drop runtime last-seen entries for printers that were removed.
776 ( new Cloud_Print_Registry() )->prune_seen( array_keys( $seen_ids ) );
777
778 $response_printers = array_map(
779 function ( $printer ) {
780 $printer = $this->with_cloud_printer_encoding_fields( $printer );
781 unset( $printer['poll_token_hash'], $printer['printnode_api_key'], $printer['star_api_key'] );
782
783 return $printer;
784 },
785 $clean_printers
786 );
787
788 return new WP_REST_Response(
789 array(
790 'printers' => $response_printers,
791 'assignments' => $clean['assignments'],
792 'generated' => $generated,
793 ),
794 200
795 );
796 }
797
798 /**
799 * Sanitize a cloud printer entry.
800 *
801 * @param mixed $printer Printer.
802 *
803 * @return array
804 */
805 private function sanitize_cloud_printer( $printer ): array {
806 $printer = \is_array( $printer ) ? $printer : array();
807 $provider = \in_array( $printer['provider'] ?? '', Provider::valid(), true )
808 ? $printer['provider'] : 'star-cloudprnt';
809
810 $clean = array(
811 'id' => sanitize_text_field( $printer['id'] ?? '' ),
812 'name' => sanitize_text_field( $printer['name'] ?? '' ),
813 'provider' => $provider,
814 'store_id' => isset( $printer['store_id'] ) ? (int) $printer['store_id'] : 0,
815 'regenerate_token' => ! empty( $printer['regenerate_token'] ),
816 );
817 if ( 'printnode' === $provider ) {
818 $clean['printnode_api_key'] = sanitize_text_field( $printer['printnode_api_key'] ?? '' );
819 $clean['printnode_printer_id'] = isset( $printer['printnode_printer_id'] ) ? (int) $printer['printnode_printer_id'] : 0;
820 $clean['printnode_format'] = \in_array( $printer['printnode_format'] ?? '', array( 'pdf', 'raw' ), true )
821 ? $printer['printnode_format'] : 'pdf';
822 }
823 if ( 'star-cloudprnt' === $provider ) {
824 $encoding_fields = array_intersect_key(
825 $printer,
826 array_flip( array( 'columns', 'language', 'autoCut', 'fullReceiptRaster' ) )
827 );
828 $clean = $this->with_cloud_printer_encoding_fields(
829 array_merge( $clean, $encoding_fields )
830 );
831 }
832 if ( 'star-online' === $provider ) {
833 $clean['star_api_key'] = sanitize_text_field( $printer['star_api_key'] ?? '' );
834 $clean['star_cloudprnt_url'] = esc_url_raw( $printer['star_cloudprnt_url'] ?? '' );
835 $clean['star_device_id'] = sanitize_text_field( $printer['star_device_id'] ?? '' );
836 $clean['star_client_type'] = sanitize_text_field( $printer['star_client_type'] ?? '' );
837 }
838
839 return $clean;
840 }
841
842 /**
843 * Add server-owned client encoding fields for Star CloudPRNT printers.
844 *
845 * These fields let POS clients synthesize read-only cloud printer targets
846 * without guessing how to render raw payloads before CloudPRNT delivery.
847 *
848 * @param array $printer Printer row.
849 *
850 * @return array
851 */
852 private function with_cloud_printer_encoding_fields( array $printer ): array {
853 if ( 'star-cloudprnt' !== ( $printer['provider'] ?? '' ) ) {
854 return $printer;
855 }
856
857 // No Star CloudPRNT printer decodes ESC/POS (the TSP100IV prints the
858 // command bytes as literal text), so 'esc-pos' is not an accepted
859 // value — including rows where earlier releases materialized it into
860 // the stored option as the old default. Only 'star-line' survives as
861 // an explicit choice, for Line Mode-only models (TSP650II et al.).
862 // There is no admin toggle; sites that need a different fallback set
863 // it in code via the woocommerce_pos_cloud_printer_default_language filter.
864 $accepted = array( 'star-prnt', 'star-line' );
865 $language = $printer['language'] ?? '';
866 if ( ! \in_array( $language, $accepted, true ) ) {
867 $language = (string) apply_filters( 'woocommerce_pos_cloud_printer_default_language', 'star-prnt', $printer );
868 if ( ! \in_array( $language, $accepted, true ) ) {
869 $language = 'star-prnt';
870 }
871 }
872 $columns = isset( $printer['columns'] ) ? (int) $printer['columns'] : 42;
873 if ( ! \in_array( $columns, array( 32, 42, 48 ), true ) ) {
874 $columns = 42;
875 }
876
877 $printer['columns'] = $columns;
878 $printer['language'] = $language;
879 $printer['autoCut'] = array_key_exists( 'autoCut', $printer ) ? rest_sanitize_boolean( $printer['autoCut'] ) : true;
880 $printer['fullReceiptRaster'] = array_key_exists( 'fullReceiptRaster', $printer ) ? rest_sanitize_boolean( $printer['fullReceiptRaster'] ) : false;
881
882 return $printer;
883 }
884
885 /**
886 * Sanitize a cloud assignment entry.
887 *
888 * @param mixed $assignment Assignment.
889 *
890 * @return array
891 */
892 private function sanitize_cloud_assignment( $assignment ): array {
893 $assignment = \is_array( $assignment ) ? $assignment : array();
894
895 return array(
896 'printer_id' => sanitize_text_field( $assignment['printer_id'] ?? '' ),
897 'store_id' => isset( $assignment['store_id'] ) ? (int) $assignment['store_id'] : 0,
898 'scope' => \in_array( $assignment['scope'] ?? '', array( 'every', 'pos', 'online' ), true ) ? $assignment['scope'] : 'every',
899 'template_id' => sanitize_text_field( (string) ( $assignment['template_id'] ?? '' ) ),
900 );
901 }
902
903
904 /**
905 * Update payment gateways settings.
906 *
907 * @param WP_REST_Request $request Full details about the request.
908 *
909 * @return array|WP_Error
910 */
911 public function update_payment_gateways_settings( WP_REST_Request $request ) {
912 $old_settings = woocommerce_pos_get_settings( 'payment_gateways' );
913 $updated_settings = array_replace_recursive( $old_settings, $request->get_json_params() );
914
915 $settings_service = SettingsService::instance();
916
917 return $settings_service->save_settings( 'payment_gateways', $updated_settings );
918 }
919
920 /**
921 * POST data comes in as PATCH, ie: partial, so we need to merge with existing data.
922 *
923 * @TODO - shouldn't the update return a WP_REST_Response?
924 *
925 * @param WP_REST_Request $request Full details about the request.
926 *
927 * @return array|WP_Error
928 */
929 public function update_general_settings( WP_REST_Request $request ) {
930 $old_settings = woocommerce_pos_get_settings( 'general' );
931 $payload = $request->get_json_params();
932 $settings = array_replace_recursive( $old_settings, $payload );
933 if ( isset( $payload['store_tax_ids'] ) && \is_array( $payload['store_tax_ids'] ) ) {
934 $settings['store_tax_ids'] = $payload['store_tax_ids'];
935 }
936
937 $settings_service = SettingsService::instance();
938
939 return $settings_service->save_settings( 'general', $settings );
940 }
941
942
943
944 /**
945 * Update checkout settings.
946 *
947 * @TODO - shouldn't the update return a WP_REST_Response?
948 *
949 * @param WP_REST_Request $request Full details about the request.
950 *
951 * @return array|WP_Error
952 */
953 public function update_checkout_settings( WP_REST_Request $request ) {
954 $old_settings = woocommerce_pos_get_settings( 'checkout' );
955 $settings = array_replace_recursive( $old_settings, $request->get_json_params() );
956
957 $settings_service = SettingsService::instance();
958
959 return $settings_service->save_settings( 'checkout', $settings );
960 }
961
962
963
964 /**
965 * Update access settings.
966 *
967 * @TODO - shouldn't the update return a WP_REST_Response?
968 *
969 * @param WP_REST_Request $request Full details about the request.
970 *
971 * @return array
972 */
973 public function update_access_settings( WP_REST_Request $request ): array {
974 global $wp_roles;
975 $data = $request->get_json_params();
976
977 // get all role slugs.
978 $roles = array_keys( $wp_roles->roles );
979
980 // get property from $data where key is in $roles.
981 $update = array_intersect_key( $data, array_flip( $roles ) );
982
983 // if $role is array with one property, update the capabilities.
984 if ( 1 === \count( $update ) ) {
985 $slugs = array_keys( $update );
986 $slug = $slugs[0];
987 $role = get_role( $slug );
988
989 // flatten capabilities array from 'wc', 'wp', 'wcpos' grouping.
990 $flattened_caps = array();
991 foreach ( $update[ $slug ]['capabilities'] as $capabilities ) {
992 $flattened_caps = array_merge( $flattened_caps, $capabilities );
993 }
994
995 // update capabilities for each $flattened_cap (should only be one).
996 foreach ( $flattened_caps as $cap => $grant ) {
997 // sanity check for admin role, read capability.
998 if ( 'administrator' === $slug && 'read' === $cap ) {
999 continue;
1000 }
1001 if ( $grant ) {
1002 $role->add_cap( $cap );
1003 } else {
1004 $role->remove_cap( $cap );
1005 }
1006 }
1007 }
1008
1009 return woocommerce_pos_get_settings( 'access' );
1010 }
1011
1012 /**
1013 * POST data comes in as PATCH, ie: partial, so we need to merge with existing data.
1014 *
1015 * @TODO - shouldn't the update return a WP_REST_Response?
1016 *
1017 * @param WP_REST_Request $request Full details about the request.
1018 *
1019 * @return array|WP_Error
1020 */
1021 public function update_tools_settings( WP_REST_Request $request ) {
1022 $old_settings = woocommerce_pos_get_settings( 'tools' );
1023 $settings = array_replace_recursive( $old_settings, $request->get_json_params() );
1024
1025 $settings_service = SettingsService::instance();
1026
1027 return $settings_service->save_settings( 'tools', $settings );
1028 }
1029
1030 /**
1031 * Check read permissions.
1032 *
1033 * @TODO - who can read settings?
1034 *
1035 * @return bool
1036 */
1037 public function read_permission_check(): bool {
1038 return current_user_can( 'manage_woocommerce_pos' );
1039 }
1040
1041 /**
1042 * Check Cloud Print read permissions.
1043 *
1044 * POS clients need to read server-owned Cloud Printer targets so they can route
1045 * receipts to printers configured by a manager. Updating the server-owned
1046 * settings still requires manage_woocommerce_pos via update_permission_check().
1047 *
1048 * @return bool
1049 */
1050 public function cloud_print_read_permission_check(): bool {
1051 return current_user_can( 'access_woocommerce_pos' );
1052 }
1053
1054 /**
1055 * Check update permissions.
1056 *
1057 * @return bool
1058 */
1059 public function update_permission_check(): bool {
1060 return current_user_can( 'manage_woocommerce_pos' );
1061 }
1062
1063 /**
1064 * Check access update permissions.
1065 *
1066 * @return bool
1067 */
1068 public function update_access_permission_check(): bool {
1069 return current_user_can( 'edit_users' ) && current_user_can( 'promote_users' );
1070 }
1071
1072 /**
1073 * Filter payment gateways settings.
1074 *
1075 * @param mixed $options The gateway options.
1076 */
1077 public function payment_gateways_settings( $options ) {
1078 foreach ( $options['gateways'] as $gateway_id => &$gateway_data ) {
1079 if ( ! \in_array( $gateway_id, array( 'pos_cash', 'pos_card' ), true ) ) {
1080 $gateway_data['enabled'] = false;
1081 }
1082 }
1083 if ( ! \in_array( $options['default_gateway'], array( 'pos_cash', 'pos_card' ), true ) ) {
1084 $options['default_gateway'] = 'pos_cash';
1085 }
1086
1087 return $options;
1088 }
1089
1090 /**
1091 * Temporary fix for stale license status transient. Remove when possible.
1092 *
1093 * @param mixed $value The option value.
1094 */
1095 public function remove_license_transient( $value ) {
1096 delete_transient( 'woocommerce_pos_pro_license_status' );
1097
1098 return $value;
1099 }
1100 }
1101