SettingsOptions   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 561
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 283
dl 0
loc 561
rs 8.4
c 0
b 0
f 0
wmc 50

15 Methods

Rating   Name   Duplication   Size   Complexity  
A get_item() 0 10 2
B get_item_schema() 0 91 1
A register_routes() 0 70 1
A get_countries_and_states() 0 18 5
A filter_setting() 0 15 3
A is_setting_type_valid() 0 15 1
A allowed_setting_keys() 0 15 1
C get_group_settings() 0 51 12
A get_data_for_response() 0 3 1
A prepare_links() 0 12 1
A get_items() 0 18 4
A get_batch_of_items_from_request() 0 20 4
A cast_image_width() 0 9 3
A get_setting() 0 30 6
A update_item() 0 33 5

How to fix   Complexity   

Complex Class

Complex classes like SettingsOptions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SettingsOptions, and based on these observations, apply Extract Interface, too.

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

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

331
			$value = $this->validate_setting_text_field( $request['value'], /** @scrutinizer ignore-type */ $setting );
Loading history...
332
		}
333
334
		if ( is_wp_error( $value ) ) {
335
			return $value;
336
		}
337
338
		if ( is_array( $setting['option_key'] ) ) {
339
			$setting['value']       = $value;
340
			$option_key             = $setting['option_key'];
341
			$prev                   = get_option( $option_key[0] );
342
			$prev[ $option_key[1] ] = $request['value'];
343
			update_option( $option_key[0], $prev );
344
		} else {
345
			$update_data                           = array();
346
			$update_data[ $setting['option_key'] ] = $value;
347
			$setting['value']                      = $value;
348
			\WC_Admin_Settings::save_fields( array( $setting ), $update_data );
349
		}
350
351
		$response = $this->prepare_item_for_response( $setting, $request );
352
353
		return rest_ensure_response( $response );
354
	}
355
356
	/**
357
	 * Get data for this object in the format of this endpoint's schema.
358
	 *
359
	 * @param \WC_Shipping_Method $object Object to prepare.
360
	 * @param \WP_REST_Request    $request Request object.
361
	 * @return array Array of data in the correct format.
362
	 */
363
	protected function get_data_for_response( $object, $request ) {
364
		unset( $object['option_key'] );
365
		return $this->filter_setting( $object );
0 ignored issues
show
Bug introduced by
$object of type WC_Shipping_Method is incompatible with the type array expected by parameter $setting of Automattic\WooCommerce\R...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

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