Completed
Push — master ( 9a7f47...7f2ea5 )
by Mike
05:50
created

SettingsOptions::get_batch_of_items_from_request()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 2
dl 0
loc 20
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * REST API Setting Options controller
4
 *
5
 * Handles requests to the /settings/$group/$setting endpoints.
6
 *
7
 * @package WooCommerce/RestApi
8
 */
9
10
namespace WooCommerce\RestApi\Controllers\Version4;
11
12
defined( 'ABSPATH' ) || exit;
13
14
/**
15
 * REST API Setting Options controller class.
16
 */
17
class SettingsOptions extends AbstractController {
18
	use SettingsTrait;
19
20
	/**
21
	 * Route base.
22
	 *
23
	 * @var string
24
	 */
25
	protected $rest_base = 'settings/(?P<group_id>[\w-]+)';
26
27
	/**
28
	 * Register routes.
29
	 *
30
	 * @since 3.0.0
31
	 */
32
	public function register_routes() {
33
		register_rest_route(
34
			$this->namespace,
35
			'/' . $this->rest_base,
36
			array(
37
				'args'   => array(
38
					'group' => array(
39
						'description' => __( 'Settings group ID.', 'woocommerce' ),
40
						'type'        => 'string',
41
					),
42
				),
43
				array(
44
					'methods'             => \WP_REST_Server::READABLE,
45
					'callback'            => array( $this, 'get_items' ),
46
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
47
				),
48
				'schema' => array( $this, 'get_public_item_schema' ),
49
			),
50
			true
51
		);
52
53
		register_rest_route(
54
			$this->namespace,
55
			'/' . $this->rest_base . '/batch',
56
			array(
57
				'args'   => array(
58
					'group' => array(
59
						'description' => __( 'Settings group ID.', 'woocommerce' ),
60
						'type'        => 'string',
61
					),
62
				),
63
				array(
64
					'methods'             => \WP_REST_Server::EDITABLE,
65
					'callback'            => array( $this, 'batch_items' ),
66
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
67
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
68
				),
69
				'schema' => array( $this, 'get_public_batch_schema' ),
70
			),
71
			true
72
		);
73
74
		register_rest_route(
75
			$this->namespace,
76
			'/' . $this->rest_base . '/(?P<id>[\w-]+)',
77
			array(
78
				'args'   => array(
79
					'group' => array(
80
						'description' => __( 'Settings group ID.', 'woocommerce' ),
81
						'type'        => 'string',
82
					),
83
					'id'    => array(
84
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
85
						'type'        => 'string',
86
					),
87
				),
88
				array(
89
					'methods'             => \WP_REST_Server::READABLE,
90
					'callback'            => array( $this, 'get_item' ),
91
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
92
				),
93
				array(
94
					'methods'             => \WP_REST_Server::EDITABLE,
95
					'callback'            => array( $this, 'update_item' ),
96
					'permission_callback' => array( $this, 'update_items_permissions_check' ),
97
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
98
				),
99
				'schema' => array( $this, 'get_public_item_schema' ),
100
			),
101
			true
102
		);
103
	}
104
105
	/**
106
	 * Return a single setting.
107
	 *
108
	 * @since  3.0.0
109
	 * @param  \WP_REST_Request $request Request data.
110
	 * @return \WP_Error|\WP_REST_Response
111
	 */
112
	public function get_item( $request ) {
113
		$setting = $this->get_setting( $request['group_id'], $request['id'] );
114
115
		if ( is_wp_error( $setting ) ) {
116
			return $setting;
117
		}
118
119
		$response = $this->prepare_item_for_response( $setting, $request );
120
121
		return rest_ensure_response( $response );
122
	}
123
124
	/**
125
	 * Return all settings in a group.
126
	 *
127
	 * @since  3.0.0
128
	 * @param  \WP_REST_Request $request Request data.
129
	 * @return \WP_Error|\WP_REST_Response
130
	 */
131
	public function get_items( $request ) {
132
		$settings = $this->get_group_settings( $request['group_id'] );
133
134
		if ( is_wp_error( $settings ) ) {
135
			return $settings;
136
		}
137
138
		$data = array();
139
140
		foreach ( $settings as $setting_obj ) {
141
			$setting = $this->prepare_item_for_response( $setting_obj, $request );
142
			$setting = $this->prepare_response_for_collection( $setting );
143
			if ( $this->is_setting_type_valid( $setting['type'] ) ) {
144
				$data[] = $setting;
145
			}
146
		}
147
148
		return rest_ensure_response( $data );
149
	}
150
151
	/**
152
	 * Get all settings in a group.
153
	 *
154
	 * @param string $group_id Group ID.
155
	 * @return array|\WP_Error
156
	 */
157
	public function get_group_settings( $group_id ) {
158
		if ( empty( $group_id ) ) {
159
			return new \WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
160
		}
161
162
		$settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
163
164
		if ( empty( $settings ) ) {
165
			return new \WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
166
		}
167
168
		$filtered_settings = array();
169
		foreach ( $settings as $setting ) {
170
			$option_key = $setting['option_key'];
171
			$setting    = $this->filter_setting( $setting );
172
			$default    = isset( $setting['default'] ) ? $setting['default'] : '';
173
			// Get the option value.
174
			if ( is_array( $option_key ) ) {
175
				$option           = get_option( $option_key[0] );
176
				$setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
177
			} else {
178
				$admin_setting_value = \WC_Admin_Settings::get_option( $option_key, $default );
179
				$setting['value']    = $admin_setting_value;
180
			}
181
182
			if ( 'multi_select_countries' === $setting['type'] ) {
183
				$setting['options'] = WC()->countries->get_countries();
184
				$setting['type']    = 'multiselect';
185
			} elseif ( 'single_select_country' === $setting['type'] ) {
186
				$setting['type']    = 'select';
187
				$setting['options'] = $this->get_countries_and_states();
188
			} elseif ( 'single_select_page' === $setting['type'] ) {
189
				$pages   = get_pages(
190
					array(
191
						'sort_column'  => 'menu_order',
192
						'sort_order'   => 'ASC',
193
						'hierarchical' => 0,
194
					)
195
				);
196
				$options = array();
197
				foreach ( $pages as $page ) {
198
					$options[ $page->ID ] = ! empty( $page->post_title ) ? $page->post_title : '#' . $page->ID;
199
				}
200
				$setting['type']    = 'select';
201
				$setting['options'] = $options;
202
			}
203
204
			$filtered_settings[] = $setting;
205
		}
206
207
		return $filtered_settings;
208
	}
209
210
	/**
211
	 * Returns a list of countries and states for use in the base location setting.
212
	 *
213
	 * @since  3.0.7
214
	 * @return array Array of states and countries.
215
	 */
216
	private function get_countries_and_states() {
217
		$countries = WC()->countries->get_countries();
218
		if ( empty( $countries ) ) {
219
			return array();
220
		}
221
		$output = array();
222
		foreach ( $countries as $key => $value ) {
223
			$states = WC()->countries->get_states( $key );
224
225
			if ( $states ) {
226
				foreach ( $states as $state_key => $state_value ) {
227
					$output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value;
228
				}
229
			} else {
230
				$output[ $key ] = $value;
231
			}
232
		}
233
		return $output;
234
	}
235
236
	/**
237
	 * Get setting data.
238
	 *
239
	 * @since  3.0.0
240
	 * @param string $group_id Group ID.
241
	 * @param string $setting_id Setting ID.
242
	 * @return stdClass|\WP_Error
0 ignored issues
show
Bug introduced by
The type WooCommerce\RestApi\Controllers\Version4\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
243
	 */
244
	public function get_setting( $group_id, $setting_id ) {
245
		if ( empty( $setting_id ) ) {
246
			return new \WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
247
		}
248
249
		$settings = $this->get_group_settings( $group_id );
250
251
		if ( is_wp_error( $settings ) ) {
252
			return $settings;
253
		}
254
255
		$array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id );
0 ignored issues
show
Bug introduced by
$settings of type WP_Error is incompatible with the type array expected by parameter $list of wp_list_pluck(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
		$array_key = array_keys( wp_list_pluck( /** @scrutinizer ignore-type */ $settings, 'id' ), $setting_id );
Loading history...
256
257
		if ( empty( $array_key ) ) {
258
			return new \WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
259
		}
260
261
		$setting = $settings[ $array_key[0] ];
262
263
		if ( ! $this->is_setting_type_valid( $setting['type'] ) ) {
264
			return new \WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
265
		}
266
267
		if ( is_wp_error( $setting ) ) {
268
			return $setting;
269
		}
270
271
		$setting['group_id'] = $group_id;
272
273
		return $setting;
274
	}
275
276
	/**
277
	 * Get batch of items from requst.
278
	 *
279
	 * @param \WP_REST_Request $request Full details about the request.
280
	 * @param string           $batch_type Batch type; one of create, update, delete.
281
	 * @return array
282
	 */
283
	protected function get_batch_of_items_from_request( $request, $batch_type ) {
284
		$params = $request->get_params();
285
286
		if ( ! isset( $params[ $batch_type ] ) ) {
287
			return array();
288
		}
289
290
		/**
291
		 * Since our batch settings update is group-specific and matches based on the route,
292
		 * we inject the URL parameters (containing group) into the batch items
293
		 */
294
		$items = array_filter( $params[ $batch_type ] );
295
296
		if ( 'update' === $batch_type ) {
297
			foreach ( $items as $key => $item ) {
298
				$items[ $key ] = array_merge( $request->get_url_params(), $item );
299
			}
300
		}
301
302
		return array_filter( $items );
303
	}
304
305
	/**
306
	 * Update a single setting in a group.
307
	 *
308
	 * @since  3.0.0
309
	 * @param  \WP_REST_Request $request Request data.
310
	 * @return \WP_Error|\WP_REST_Response
311
	 */
312
	public function update_item( $request ) {
313
		$setting = $this->get_setting( $request['group_id'], $request['id'] );
314
315
		if ( is_wp_error( $setting ) ) {
316
			return $setting;
317
		}
318
319
		if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) {
320
			$value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting );
321
		} else {
322
			$value = $this->validate_setting_text_field( $request['value'], $setting );
0 ignored issues
show
Bug introduced by
$setting of type WP_Error|WooCommerce\Res...llers\Version4\stdClass is incompatible with the type array expected by parameter $setting of WooCommerce\RestApi\Cont...te_setting_text_field(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

322
			$value = $this->validate_setting_text_field( $request['value'], /** @scrutinizer ignore-type */ $setting );
Loading history...
323
		}
324
325
		if ( is_wp_error( $value ) ) {
326
			return $value;
327
		}
328
329
		if ( is_array( $setting['option_key'] ) ) {
330
			$setting['value']       = $value;
331
			$option_key             = $setting['option_key'];
332
			$prev                   = get_option( $option_key[0] );
333
			$prev[ $option_key[1] ] = $request['value'];
334
			update_option( $option_key[0], $prev );
335
		} else {
336
			$update_data                           = array();
337
			$update_data[ $setting['option_key'] ] = $value;
338
			$setting['value']                      = $value;
339
			\WC_Admin_Settings::save_fields( array( $setting ), $update_data );
340
		}
341
342
		$response = $this->prepare_item_for_response( $setting, $request );
343
344
		return rest_ensure_response( $response );
345
	}
346
347
	/**
348
	 * Prepare a single setting object for response.
349
	 *
350
	 * @param object           $item Setting object.
351
	 * @param \WP_REST_Request $request Request object.
352
	 * @return \WP_REST_Response $response Response data.
353
	 */
354
	public function prepare_item_for_response( $item, $request ) {
355
		unset( $item['option_key'] );
356
		$data     = $this->filter_setting( $item );
0 ignored issues
show
Bug introduced by
$item of type object is incompatible with the type array expected by parameter $setting of WooCommerce\RestApi\Cont...tions::filter_setting(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

356
		$data     = $this->filter_setting( /** @scrutinizer ignore-type */ $item );
Loading history...
357
		$data     = $this->add_additional_fields_to_object( $data, $request );
358
		$data     = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] );
359
		$response = rest_ensure_response( $data );
360
		$response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) );
361
		return $response;
362
	}
363
364
	/**
365
	 * Prepare links for the request.
366
	 *
367
	 * @param string $setting_id Setting ID.
368
	 * @param string $group_id Group ID.
369
	 * @return array Links for the given setting.
370
	 */
371
	protected function prepare_links( $setting_id, $group_id ) {
372
		$base  = str_replace( '(?P<group_id>[\w-]+)', $group_id, $this->rest_base );
373
		$links = array(
374
			'self'       => array(
375
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ),
376
			),
377
			'collection' => array(
378
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
379
			),
380
		);
381
382
		return $links;
383
	}
384
385
	/**
386
	 * Makes sure the current user has access to READ the settings APIs.
387
	 *
388
	 * @since  3.0.0
389
	 * @param \WP_REST_Request $request Full data about the request.
390
	 * @return \WP_Error|boolean
391
	 */
392
	public function get_items_permissions_check( $request ) {
393
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
394
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
395
		}
396
397
		return true;
398
	}
399
400
	/**
401
	 * Makes sure the current user has access to WRITE the settings APIs.
402
	 *
403
	 * @since  3.0.0
404
	 * @param \WP_REST_Request $request Full data about the request.
405
	 * @return \WP_Error|boolean
406
	 */
407
	public function update_items_permissions_check( $request ) {
408
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
409
			return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
410
		}
411
412
		return true;
413
	}
414
415
	/**
416
	 * Filters out bad values from the settings array/filter so we
417
	 * only return known values via the API.
418
	 *
419
	 * @since 3.0.0
420
	 * @param  array $setting Settings.
421
	 * @return array
422
	 */
423
	public function filter_setting( $setting ) {
424
		$setting = array_intersect_key(
425
			$setting,
426
			array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) )
427
		);
428
429
		if ( empty( $setting['options'] ) ) {
430
			unset( $setting['options'] );
431
		}
432
433
		if ( 'image_width' === $setting['type'] ) {
434
			$setting = $this->cast_image_width( $setting );
435
		}
436
437
		return $setting;
438
	}
439
440
	/**
441
	 * For image_width, Crop can return "0" instead of false -- so we want
442
	 * to make sure we return these consistently the same we accept them.
443
	 *
444
	 * @todo remove in 4.0
445
	 * @since 3.0.0
446
	 * @param  array $setting Settings.
447
	 * @return array
448
	 */
449
	public function cast_image_width( $setting ) {
450
		foreach ( array( 'default', 'value' ) as $key ) {
451
			if ( isset( $setting[ $key ] ) ) {
452
				$setting[ $key ]['width']  = intval( $setting[ $key ]['width'] );
453
				$setting[ $key ]['height'] = intval( $setting[ $key ]['height'] );
454
				$setting[ $key ]['crop']   = (bool) $setting[ $key ]['crop'];
455
			}
456
		}
457
		return $setting;
458
	}
459
460
	/**
461
	 * Callback for allowed keys for each setting response.
462
	 *
463
	 * @param  string $key Key to check.
464
	 * @return boolean
465
	 */
466
	public function allowed_setting_keys( $key ) {
467
		return in_array(
468
			$key, array(
469
				'id',
470
				'group_id',
471
				'label',
472
				'description',
473
				'default',
474
				'tip',
475
				'placeholder',
476
				'type',
477
				'options',
478
				'value',
479
				'option_key',
480
			), true
481
		);
482
	}
483
484
	/**
485
	 * Boolean for if a setting type is a valid supported setting type.
486
	 *
487
	 * @since  3.0.0
488
	 * @param  string $type Type.
489
	 * @return bool
490
	 */
491
	public function is_setting_type_valid( $type ) {
492
		return in_array(
493
			$type, array(
494
				'text',         // Validates with validate_setting_text_field.
495
				'email',        // Validates with validate_setting_text_field.
496
				'number',       // Validates with validate_setting_text_field.
497
				'color',        // Validates with validate_setting_text_field.
498
				'password',     // Validates with validate_setting_text_field.
499
				'textarea',     // Validates with validate_setting_textarea_field.
500
				'select',       // Validates with validate_setting_select_field.
501
				'multiselect',  // Validates with validate_setting_multiselect_field.
502
				'radio',        // Validates with validate_setting_radio_field (-> validate_setting_select_field).
503
				'checkbox',     // Validates with validate_setting_checkbox_field.
504
				'image_width',  // Validates with validate_setting_image_width_field.
505
				'thumbnail_cropping', // Validates with validate_setting_text_field.
506
			)
507
		);
508
	}
509
510
	/**
511
	 * Get the settings schema, conforming to JSON Schema.
512
	 *
513
	 * @return array
514
	 */
515
	public function get_item_schema() {
516
		$schema = array(
517
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
518
			'title'      => 'setting',
519
			'type'       => 'object',
520
			'properties' => array(
521
				'id'          => array(
522
					'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
523
					'type'        => 'string',
524
					'arg_options' => array(
525
						'sanitize_callback' => 'sanitize_title',
526
					),
527
					'context'     => array( 'view', 'edit' ),
528
					'readonly'    => true,
529
				),
530
				'group_id'    => array(
531
					'description' => __( 'An identifier for the group this setting belongs to.', 'woocommerce' ),
532
					'type'        => 'string',
533
					'arg_options' => array(
534
						'sanitize_callback' => 'sanitize_title',
535
					),
536
					'context'     => array( 'view', 'edit' ),
537
					'readonly'    => true,
538
				),
539
				'label'       => array(
540
					'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
541
					'type'        => 'string',
542
					'arg_options' => array(
543
						'sanitize_callback' => 'sanitize_text_field',
544
					),
545
					'context'     => array( 'view', 'edit' ),
546
					'readonly'    => true,
547
				),
548
				'description' => array(
549
					'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
550
					'type'        => 'string',
551
					'arg_options' => array(
552
						'sanitize_callback' => 'sanitize_text_field',
553
					),
554
					'context'     => array( 'view', 'edit' ),
555
					'readonly'    => true,
556
				),
557
				'value'       => array(
558
					'description' => __( 'Setting value.', 'woocommerce' ),
559
					'type'        => 'mixed',
560
					'context'     => array( 'view', 'edit' ),
561
				),
562
				'default'     => array(
563
					'description' => __( 'Default value for the setting.', 'woocommerce' ),
564
					'type'        => 'mixed',
565
					'context'     => array( 'view', 'edit' ),
566
					'readonly'    => true,
567
				),
568
				'tip'         => array(
569
					'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
570
					'type'        => 'string',
571
					'arg_options' => array(
572
						'sanitize_callback' => 'sanitize_text_field',
573
					),
574
					'context'     => array( 'view', 'edit' ),
575
					'readonly'    => true,
576
				),
577
				'placeholder' => array(
578
					'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
579
					'type'        => 'string',
580
					'arg_options' => array(
581
						'sanitize_callback' => 'sanitize_text_field',
582
					),
583
					'context'     => array( 'view', 'edit' ),
584
					'readonly'    => true,
585
				),
586
				'type'        => array(
587
					'description' => __( 'Type of setting.', 'woocommerce' ),
588
					'type'        => 'string',
589
					'arg_options' => array(
590
						'sanitize_callback' => 'sanitize_text_field',
591
					),
592
					'context'     => array( 'view', 'edit' ),
593
					'enum'        => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
594
					'readonly'    => true,
595
				),
596
				'options'     => array(
597
					'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
598
					'type'        => 'object',
599
					'context'     => array( 'view', 'edit' ),
600
					'readonly'    => true,
601
				),
602
			),
603
		);
604
605
		return $this->add_additional_fields_schema( $schema );
606
	}
607
}
608