Completed
Push — master ( 9a3784...c8ddca )
by Justin
22:39
created

WC_REST_Settings_Options_Controller   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 495
Duplicated Lines 9.29 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 5
Bugs 0 Features 3
Metric Value
dl 46
loc 495
rs 8.3673
c 5
b 0
f 3
wmc 45
lcom 1
cbo 4

16 Methods

Rating   Name   Duplication   Size   Complexity  
B register_routes() 0 35 1
A get_item() 0 11 2
A get_items() 19 19 4
D get_group_settings() 0 35 9
B get_setting() 0 25 5
A batch_items() 0 19 3
B update_item() 0 34 5
A prepare_item_for_response() 0 9 2
A prepare_links() 13 13 1
A get_items_permissions_check() 7 7 2
A update_items_permissions_check() 7 7 2
A filter_setting() 0 16 3
A cast_image_width() 0 10 3
A allowed_setting_keys() 0 14 1
A is_setting_type_valid() 0 15 1
B get_item_schema() 0 82 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_REST_Settings_Options_Controller 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 WC_REST_Settings_Options_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * REST API Settings Controller.
8
 * Handles requests to the /settings/$group/$setting endpoints.
9
 *
10
 * @author   WooThemes
11
 * @category API
12
 * @package  WooCommerce/API
13
 * @version  2.7.0
14
 * @since    2.7.0
15
 */
16
class WC_REST_Settings_Options_Controller extends WC_REST_Controller {
17
18
	/**
19
	 * WP REST API namespace/version.
20
	 */
21
	protected $namespace = 'wc/v1';
22
23
	/**
24
	 * Route base.
25
	 *
26
	 * @var string
27
	 */
28
	protected $rest_base = 'settings';
29
30
	/**
31
	 * Register routes.
32
	 *
33
	 * @since 2.7.0
34
	 */
35
	public function register_routes() {
36
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<group>[\w-]+)', array(
37
			array(
38
				'methods'             => WP_REST_Server::READABLE,
39
				'callback'            => array( $this, 'get_items' ),
40
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
41
			),
42
			'schema' => array( $this, 'get_public_item_schema' ),
43
		) );
44
45
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<group>[\w-]+)/batch', array(
46
			array(
47
				'methods'             => WP_REST_Server::EDITABLE,
48
				'callback'            => array( $this, 'batch_items' ),
49
				'permission_callback' => array( $this, 'update_items_permissions_check' ),
50
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
51
			),
52
			'schema' => array( $this, 'get_public_batch_schema' ),
53
		) );
54
55
		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<group>[\w-]+)/(?P<id>[\w-]+)', array(
56
			array(
57
				'methods'             => WP_REST_Server::READABLE,
58
				'callback'            => array( $this, 'get_item' ),
59
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
60
			),
61
			array(
62
				'methods'             => WP_REST_Server::EDITABLE,
63
				'callback'            => array( $this, 'update_item' ),
64
				'permission_callback' => array( $this, 'update_items_permissions_check' ),
65
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
66
			),
67
			'schema' => array( $this, 'get_public_item_schema' ),
68
		) );
69
	}
70
71
	/**
72
	 * Return a single setting.
73
	 *
74
	 * @since  2.7.0
75
	 * @param  WP_REST_Request $request
76
	 * @return WP_Error|WP_REST_Response
77
	 */
78
	public function get_item( $request ) {
79
		$setting = $this->get_setting( $request['group'], $request['id'] );
80
81
		if ( is_wp_error( $setting ) ) {
82
			return $setting;
83
		}
84
85
		$response = $this->prepare_item_for_response( $setting, $request );
86
87
		return rest_ensure_response( $response );
88
	}
89
90
	/**
91
	 * Return all settings in a group.
92
	 *
93
	 * @since  2.7.0
94
	 * @param  WP_REST_Request $request
95
	 * @return WP_Error|WP_REST_Response
96
	 */
97 View Code Duplication
	public function get_items( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
98
		$settings = $this->get_group_settings( $request['group'] );
99
100
		if ( is_wp_error( $settings ) ) {
101
			return $settings;
102
		}
103
104
		$data = array();
105
106
		foreach ( $settings as $setting_obj ) {
107
			$setting = $this->prepare_item_for_response( $setting_obj, $request );
108
			$setting = $this->prepare_response_for_collection( $setting );
109
			if ( $this->is_setting_type_valid( $setting['type'] ) ) {
110
				$data[]  = $setting;
111
			}
112
		}
113
114
		return rest_ensure_response( $data );
115
	}
116
117
	/**
118
	 * Get all settings in a group.
119
	 *
120
	 * @since  2.7.0
121
	 * @param string $group_id Group ID.
122
	 * @return array|WP_Error
123
	 */
124
	public function get_group_settings( $group_id ) {
125
		if ( empty( $group_id ) ) {
126
			return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
127
		}
128
129
		$settings = apply_filters( 'woocommerce_settings-' . $group_id, array() );
130
131
		if ( empty( $settings ) ) {
132
			return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
133
		}
134
135
		$filtered_settings = array();
136
		foreach ( $settings as $setting ) {
137
			$option_key = $setting['option_key'];
138
			$setting    = $this->filter_setting( $setting );
139
			$default    = isset( $setting['default'] ) ? $setting['default'] : '';
140
			// Get the option value
141
			if ( is_array( $option_key ) ) {
142
				$option           = get_option( $option_key[0] );
143
				$setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
144
			} else {
145
				$admin_setting_value = WC_Admin_Settings::get_option( $option_key );
146
				$setting['value']    = empty( $admin_setting_value ) ? $default : $admin_setting_value;
147
			}
148
149
			if ( 'multi_select_countries' === $setting['type'] ) {
150
				$setting['options'] = WC()->countries->get_countries();
151
				$setting['type']    = 'multiselect';
152
			}
153
154
			$filtered_settings[] = $setting;
155
		}
156
157
		return $filtered_settings;
158
	}
159
160
	/**
161
	 * Get setting data.
162
	 *
163
	 * @since  2.7.0
164
	 * @param string $group_id Group ID.
165
	 * @param string $setting_id Setting ID.
166
	 * @return stdClass|WP_Error
167
	 */
168
	public function get_setting( $group_id, $setting_id ) {
169
		if ( empty( $setting_id ) ) {
170
			return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
171
		}
172
173
		$settings = $this->get_group_settings( $group_id );
174
175
		if ( is_wp_error( $settings ) ) {
176
			return $settings;
177
		}
178
179
		$array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id );
180
181
		if ( empty( $array_key ) ) {
182
			return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
183
		}
184
185
		$setting = $settings[ $array_key[0] ];
186
187
		if ( ! $this->is_setting_type_valid( $setting['type'] ) ) {
188
			return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
189
		}
190
191
		return $setting;
192
	}
193
194
	/**
195
	 * Bulk create, update and delete items.
196
	 *
197
	 * @since  2.7.0
198
	 * @param WP_REST_Request $request Full details about the request.
199
	 * @return array Of WP_Error or WP_REST_Response.
200
	 */
201
	public function batch_items( $request ) {
202
		// Get the request params.
203
		$items = array_filter( $request->get_params() );
204
205
		/*
206
		 * Since our batch settings update is group-specific and matches based on the route,
207
		 * we inject the URL parameters (containing group) into the batch items
208
		 */
209
		if ( ! empty( $items['update'] ) ) {
210
			$to_update = array();
211
			foreach ( $items['update'] as $item ) {
212
				$to_update[] = array_merge( $request->get_url_params(), $item );
213
			}
214
			$request = new WP_REST_Request( $request->get_method() );
215
			$request->set_body_params( array( 'update' => $to_update ) );
216
		}
217
218
		return parent::batch_items( $request );
219
	}
220
221
	/**
222
	 * Update a single setting in a group.
223
224
	 * @since  2.7.0
225
	 * @param  WP_REST_Request $request
226
	 * @return WP_Error|WP_REST_Response
227
	 */
228
	public function update_item( $request ) {
229
		$setting = $this->get_setting( $request['group'], $request['id'] );
230
231
		if ( is_wp_error( $setting ) ) {
232
			return $setting;
233
		}
234
235
		if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) {
236
			$value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting );
237
		} else {
238
			$value = $this->validate_setting_text_field( $request['value'], $setting );
0 ignored issues
show
Bug introduced by
It seems like $setting defined by $this->get_setting($requ...roup'], $request['id']) on line 229 can also be of type object<stdClass>; however, WC_REST_Controller::validate_setting_text_field() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
239
		}
240
241
		if ( is_wp_error( $value ) ) {
242
			return $value;
243
		}
244
245
		if ( is_array( $setting['option_key'] ) ) {
246
			$setting['value']       = $value;
247
			$option_key             = $setting['option_key'];
248
			$prev                   = get_option( $option_key[0] );
249
			$prev[ $option_key[1] ] = $request['value'];
250
			update_option( $option_key[0], $prev );
251
		} else {
252
			$update_data = array();
253
			$update_data[ $setting['option_key'] ] = $value;
254
			$setting['value']                      = $value;
255
			WC_Admin_Settings::save_fields( array( $setting ), $update_data );
256
		}
257
258
		$response = $this->prepare_item_for_response( $setting, $request );
259
260
		return rest_ensure_response( $response );
261
	}
262
263
	/**
264
	 * Prepare a single setting object for response.
265
	 *
266
	 * @since  2.7.0
267
	 * @param object $item Setting object.
268
	 * @param WP_REST_Request $request Request object.
269
	 * @return WP_REST_Response $response Response data.
270
	 */
271
	public function prepare_item_for_response( $item, $request ) {
272
		unset( $item['option_key'] );
273
		$data     = $this->filter_setting( $item );
0 ignored issues
show
Documentation introduced by
$item is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
274
		$data     = $this->add_additional_fields_to_object( $data, $request );
275
		$data     = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] );
276
		$response = rest_ensure_response( $data );
277
		$response->add_links( $this->prepare_links( $data['id'], $request['group'] ) );
278
		return $response;
279
	}
280
281
	/**
282
	 * Prepare links for the request.
283
	 *
284
	 * @since  2.7.0
285
	 * @param string $setting_id Setting ID.
286
	 * @param string $group_id Group ID.
287
	 * @return array Links for the given setting.
288
	 */
289 View Code Duplication
	protected function prepare_links( $setting_id, $group_id ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
290
		$base  = '/' . $this->namespace . '/' . $this->rest_base . '/' . $group_id;
291
		$links = array(
292
			'self' => array(
293
				'href' => rest_url( trailingslashit( $base ) . $setting_id ),
294
			),
295
			'collection' => array(
296
				'href' => rest_url( $base ),
297
			),
298
		);
299
300
		return $links;
301
	}
302
303
	/**
304
	 * Makes sure the current user has access to READ the settings APIs.
305
	 *
306
	 * @since  2.7.0
307
	 * @param WP_REST_Request $request Full data about the request.
308
	 * @return WP_Error|boolean
309
	 */
310 View Code Duplication
	public function get_items_permissions_check( $request ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
311
		if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
312
			return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
313
		}
314
315
		return true;
316
	}
317
318
	/**
319
	 * Makes sure the current user has access to WRITE the settings APIs.
320
	 *
321
	 * @since  2.7.0
322
	 * @param WP_REST_Request $request Full data about the request.
323
	 * @return WP_Error|boolean
324
	 */
325 View Code Duplication
	public function update_items_permissions_check( $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
326
		if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
327
			return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
328
		}
329
330
		return true;
331
	}
332
333
	/**
334
	 * Filters out bad values from the settings array/filter so we
335
	 * only return known values via the API.
336
	 *
337
	 * @since 2.7.0
338
	 * @param  array $setting
339
	 * @return array
340
	 */
341
	public function filter_setting( $setting ) {
342
		$setting = array_intersect_key(
343
			$setting,
344
			array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) )
345
		);
346
347
		if ( empty( $setting['options'] ) ) {
348
			unset( $setting['options'] );
349
		}
350
351
		if ( 'image_width' === $setting['type'] ) {
352
			$setting = $this->cast_image_width( $setting );
353
		}
354
355
		return $setting;
356
	}
357
358
	/**
359
	 * For image_width, Crop can return "0" instead of false -- so we want
360
	 * to make sure we return these consistently the same we accept them.
361
	 *
362
	 * @since 2.7.0
363
	 * @param  array $setting
364
	 * @return array
365
	 */
366
	public function cast_image_width( $setting ) {
367
		foreach ( array( 'default', 'value' ) as $key ) {
368
			if ( isset( $setting[ $key ] ) ) {
369
				$setting[ $key ]['width']  = intval( $setting[ $key ]['width'] );
370
				$setting[ $key ]['height'] = intval( $setting[ $key ]['height'] );
371
				$setting[ $key ]['crop']   = (bool)  $setting[ $key ]['crop'];
372
			}
373
		}
374
		return $setting;
375
	}
376
377
	/**
378
	 * Callback for allowed keys for each setting response.
379
	 *
380
	 * @since  2.7.0
381
	 * @param  string $key Key to check
382
	 * @return boolean
383
	 */
384
	public function allowed_setting_keys( $key ) {
385
		return in_array( $key, array(
386
			'id',
387
			'label',
388
			'description',
389
			'default',
390
			'tip',
391
			'placeholder',
392
			'type',
393
			'options',
394
			'value',
395
			'option_key',
396
		) );
397
	}
398
399
	/**
400
	 * Boolean for if a setting type is a valid supported setting type.
401
	 *
402
	 * @since  2.7.0
403
	 * @param  string  $type
404
	 * @return boolean
405
	 */
406
	public function is_setting_type_valid( $type ) {
407
		return in_array( $type, array(
408
			'text',         // validates with validate_setting_text_field
409
			'email',        // validates with validate_setting_text_field
410
			'number',       // validates with validate_setting_text_field
411
			'color',        // validates with validate_setting_text_field
412
			'password',     // validates with validate_setting_text_field
413
			'textarea',     // validates with validate_setting_textarea_field
414
			'select',       // validates with validate_setting_select_field
415
			'multiselect',  // validates with validate_setting_multiselect_field
416
			'radio',        // validates with validate_setting_radio_field (-> validate_setting_select_field)
417
			'checkbox',     // validates with validate_setting_checkbox_field
418
			'image_width',  // validates with validate_setting_image_width_field
419
		) );
420
	}
421
422
	/**
423
	 * Get the settings schema, conforming to JSON Schema.
424
	 *
425
	 * @since 2.7.0
426
	 * @return array
427
	 */
428
	public function get_item_schema() {
429
		$schema = array(
430
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
431
			'title'                => 'setting',
432
			'type'                 => 'object',
433
			'properties'           => array(
434
				'id'               => array(
435
					'description'  => __( 'A unique identifier for the setting.', 'woocommerce' ),
436
					'type'         => 'string',
437
					'arg_options'  => array(
438
						'sanitize_callback' => 'sanitize_title',
439
					),
440
					'context'      => array( 'view', 'edit' ),
441
					'readonly'     => true,
442
				),
443
				'label'            => array(
444
					'description'  => __( 'A human readable translation wrapped label. Meant to be used in interfaces.', 'woocommerce' ),
445
					'type'         => 'string',
446
					'arg_options'  => array(
447
						'sanitize_callback' => 'sanitize_text_field',
448
					),
449
					'context'      => array( 'view', 'edit' ),
450
					'readonly'     => true,
451
				),
452
				'description'      => array(
453
					'description'  => __( 'A human readable translation wrapped description. Meant to be used in interfaces.', 'woocommerce' ),
454
					'type'         => 'string',
455
					'arg_options'  => array(
456
						'sanitize_callback' => 'sanitize_text_field',
457
					),
458
					'context'      => array( 'view', 'edit' ),
459
					'readonly'     => true,
460
				),
461
				'value'          => array(
462
					'description'  => __( 'Setting value.', 'woocommerce' ),
463
					'type'         => 'mixed',
464
					'context'      => array( 'view', 'edit' ),
465
				),
466
				'default'          => array(
467
					'description'  => __( 'Default value for the setting.', 'woocommerce' ),
468
					'type'         => 'mixed',
469
					'context'      => array( 'view', 'edit' ),
470
					'readonly'     => true,
471
				),
472
				'tip'              => array(
473
					'description'  => __( 'Extra help text explaining the setting.', 'woocommerce' ),
474
					'type'         => 'string',
475
					'arg_options'  => array(
476
						'sanitize_callback' => 'sanitize_text_field',
477
					),
478
					'context'      => array( 'view', 'edit' ),
479
					'readonly'     => true,
480
				),
481
				'placeholder'      => array(
482
					'description'  => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
483
					'type'         => 'string',
484
					'arg_options'  => array(
485
						'sanitize_callback' => 'sanitize_text_field',
486
					),
487
					'context'      => array( 'view', 'edit' ),
488
					'readonly'     => true,
489
				),
490
				'type'             => array(
491
					'description'  => __( 'Type of setting. Allowed values: text, email, number, color, password, textarea, select, multiselect, radio, image_width, checkbox.', 'woocommerce' ),
492
					'type'         => 'string',
493
					'arg_options'  => array(
494
						'sanitize_callback' => 'sanitize_text_field',
495
					),
496
					'context'      => array( 'view', 'edit' ),
497
					'readonly'     => true,
498
				),
499
				'options'          => array(
500
					'description'  => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
501
					'type'         => 'array',
502
					'context'      => array( 'view', 'edit' ),
503
					'readonly'     => true,
504
				),
505
			),
506
		);
507
508
		return $this->add_additional_fields_schema( $schema );
509
	}
510
}
511