Passed
Push — master ( 6176aa...f7c939 )
by Mike
03:08
created

SettingsOptions   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 566
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 288
dl 0
loc 566
rs 7.92
c 0
b 0
f 0
wmc 51

15 Methods

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

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 WooCommerce/RestApi
8
 */
9
10
namespace WooCommerce\RestApi\Controllers\Version4;
11
12
defined( 'ABSPATH' ) || exit;
13
14
use \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' ),
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' ),
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' ),
90
						'type'        => 'string',
91
					),
92
					'id'    => array(
93
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
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' ), 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' ), 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 WooCommerce\RestApi\Controllers\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' ), 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' ), 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' ), 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 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

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
	 * Prepare a single setting object for response.
358
	 *
359
	 * @param object           $item Setting object.
360
	 * @param \WP_REST_Request $request Request object.
361
	 * @return \WP_REST_Response $response Response data.
362
	 */
363
	public function prepare_item_for_response( $item, $request ) {
364
		unset( $item['option_key'] );
365
		$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

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