Completed
Pull Request — trunk (#578)
by Juliette
12:33
created

CMB2_Sanitize::validate_against_options()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 32
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 3
Bugs 0 Features 2
Metric Value
c 3
b 0
f 2
dl 0
loc 32
ccs 0
cts 0
cp 0
rs 8.439
cc 5
eloc 13
nc 6
nop 1
crap 30
1
<?php
2
/**
3
 * CMB2 field sanitization
4
 *
5
 * @since  0.0.4
6
 *
7
 * @category  WordPress_Plugin
8
 * @package   CMB2
9
 * @author    WebDevStudios
10
 * @license   GPL-2.0+
11
 * @link      http://webdevstudios.com
12
 *
13
 * @method string _id()
14
 */
15
class CMB2_Sanitize {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
16
17
	/**
18
	 * A CMB field object
19
	 * @var CMB2_Field object
20
	 */
21
	public $field;
22
23
	/**
24
	 * Field's value
25
	 * @var mixed
26
	 */
27
	public $value;
28
29
	/**
30
	 * Setup our class vars
31
	 * @since 1.1.0
32
	 * @param CMB2_Field $field A CMB2 field object
33
	 * @param mixed      $value Field value
34
	 */
35 9
	public function __construct( CMB2_Field $field, $value ) {
36 9
		$this->field = $field;
37 9
		$this->value = stripslashes_deep( $value ); // get rid of those evil magic quotes
38 9
	}
39
40
	/**
41
	 * Catchall method if field's 'sanitization_cb' is NOT defined, or field type does not have a corresponding validation method
42
	 * @since  1.0.0
43
	 * @param  string $name      Non-existent method name
44
	 * @param  array  $arguments All arguments passed to the method
45
	 */
46 8
	public function __call( $name, $arguments ) {
47 8
		return $this->default_sanitization();
48
	}
49
50
	/**
51
	 * Default fallback sanitization method. Applies filters.
52
	 * @since  1.0.2
53
	 */
54 8
	public function default_sanitization() {
55
56
		/**
57
		 * This exists for back-compatibility, but validation
58
		 * is not what happens here.
59
		 * @deprecated See documentation for "cmb2_sanitize_{$this->type()}".
60
		 */
61 8
		$override_value = apply_filters( "cmb2_validate_{$this->field->type()}", null, $this->value, $this->field->object_id, $this->field->args(), $this );
62
63 8
		if ( null !== $override_value ) {
64
			return $override_value;
65
		}
66
67 8
		$sanitized_value = '';
68 8
		switch ( $this->field->type() ) {
69 8
			case 'wysiwyg':
70 8
			case 'textarea_small':
71 8
			case 'oembed':
72 2
				$sanitized_value = $this->textarea();
73 2
				break;
74 8
			case 'radio':
75 8
			case 'radio_inline':
76 8
			case 'select':
77 8
				$sanitized_value = $this->validate_against_options();
78 8
				break;
79
			case 'taxonomy_select':
80
			case 'taxonomy_radio':
81
			case 'taxonomy_radio_inline':
82
			case 'taxonomy_multicheck':
83
			case 'taxonomy_multicheck_inline':
84
				if ( $this->field->args( 'taxonomy' ) ) {
85 8
					wp_set_object_terms( $this->field->object_id, $this->value, $this->field->args( 'taxonomy' ) );
86 8
				} else {
87 8
					cmb2_utils()->log_if_debug( __METHOD__, __LINE__, "{$this->field->type()} {$this->field->_id()} is missing the 'taxonomy' parameter." );
88 8
				}
89
				break;
90 2
			case 'multicheck':
91 2
			case 'multicheck_inline':
92 7
			case 'file_list':
93
			case 'group':
94
				// no filtering
95 7
				$sanitized_value = $this->value;
96 7
				break;
97 8
			default:
98
				// Handle repeatable fields array
99 8
				// We'll fallback to 'sanitize_text_field'
100
				$sanitized_value = is_array( $this->value ) ? array_map( 'sanitize_text_field', $this->value ) : sanitize_text_field( $this->value );
101
				break;
102
		}
103
104
		return $this->_is_empty_array( $sanitized_value ) ? '' : $sanitized_value;
105
	}
106
107
	/**
108
	 * Simple checkbox validation
109
	 * @since  1.0.1
110
	 * @return string|false 'on' or false
111
	 */
112
	public function checkbox() {
113
		return $this->value === 'on' ? 'on' : false;
114
	}
115
116 1
	/**
117 1
	 * Validate url in a meta value
118
	 * @since  1.0.1
119 1
	 * @return string        Empty string or escaped url
120
	 */
121
	public function text_url() {
122
		$protocols = $this->field->args( 'protocols' );
123
		// for repeatable
124 1
		if ( is_array( $this->value ) ) {
125
			foreach ( $this->value as $key => $val ) {
126
				$this->value[ $key ] = $val ? esc_url_raw( $val, $protocols ) : $this->field->args( 'default' );
127 1
			}
128
		} else {
129
			$this->value = $this->value ? esc_url_raw( $this->value, $protocols ) : $this->field->args( 'default' );
130
		}
131
132
		return $this->value;
133
	}
134
135
	public function colorpicker() {
136
		// for repeatable
137
		if ( is_array( $this->value ) ) {
138
			$check = $this->value;
139
			$this->value = array();
140
			foreach ( $check as $key => $val ) {
141
				if ( $val && '#' != $val ) {
142
					$this->value[ $key ] = esc_attr( $val );
143
				}
144
			}
145
		} else {
146
			$this->value = ! $this->value || '#' == $this->value ? '' : esc_attr( $this->value );
147
		}
148
		return $this->value;
149
	}
150
151
	/**
152
	 * Validate email in a meta value
153
	 * @since  1.0.1
154
	 * @return string       Empty string or sanitized email
155
	 */
156
	public function text_email() {
157
		// for repeatable
158
		if ( is_array( $this->value ) ) {
159
			foreach ( $this->value as $key => $val ) {
160
				$val = trim( $val );
161
				$this->value[ $key ] = is_email( $val ) ? $val : '';
162
			}
163
		} else {
164
			$this->value = trim( $this->value );
165
			$this->value = is_email( $this->value ) ? $this->value : '';
166
		}
167
168
		return $this->value;
169
	}
170
171
	/**
172
	 * Validate money in a meta value
173
	 * @since  1.0.1
174
	 * @return string       Empty string or sanitized money value
175
	 */
176
	public function text_money() {
177
178
		global $wp_locale;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
179
180
		$search = array( $wp_locale->number_format['thousands_sep'], $wp_locale->number_format['decimal_point'] );
181
		$replace = array( '', '.' );
182
183
		// for repeatable
184
		if ( is_array( $this->value ) ) {
185
			foreach ( $this->value as $key => $val ) {
186
				$this->value[ $key ] = number_format_i18n( (float) str_ireplace( $search, $replace, $val ), 2 );
187
			}
188
		} else {
189
			$this->value = number_format_i18n( (float) str_ireplace( $search, $replace, $this->value ), 2 );
190
		}
191
192
		return $this->value;
193
	}
194
195
	/**
196
	 * Converts text date to timestamp
197
	 * @since  1.0.2
198
	 * @return string Timestring
199
	 */
200
	public function text_date_timestamp() {
201
		return is_array( $this->value )
202
			? array_map( array( $this->field, 'get_timestamp_from_value' ), $this->value )
203
			: $this->field->get_timestamp_from_value( $this->value );
204
	}
205
206
	/**
207
	 * Datetime to timestamp
208
	 * @since  1.0.1
209
	 * @return string Timestring
210
	 */
211
	public function text_datetime_timestamp( $repeat = false ) {
212
213
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
214
		if ( empty( $test ) ) {
215
			return '';
216
		}
217
218
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
219
		if ( false !== $repeat_value ) {
220
			return $repeat_value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $repeat_value; (array) is incompatible with the return type documented by CMB2_Sanitize::text_datetime_timestamp of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
221
		}
222
223
		if ( isset( $this->value['date'], $this->value['time'] ) ) {
224
			$this->value = $this->field->get_timestamp_from_value( $this->value['date'] . ' ' . $this->value['time'] );
225
		}
226
227
		if ( $tz_offset = $this->field->field_timezone_offset() ) {
228
			$this->value += (int) $tz_offset;
229
		}
230
231
		return $this->value;
232
	}
233
234 1
	/**
235 1
	 * Datetime to timestamp with timezone
236
	 * @since  1.0.1
237 1
	 * @return string       Timestring
238 1
	 */
239 1
	public function text_datetime_timestamp_timezone( $repeat = false ) {
240
		static $utc_values = array();
241
242 1
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
243
		if ( empty( $test ) ) {
244 1
			return '';
245 1
		}
246 1
247
		$utc_key = $this->field->_id() . '_utc';
248
249
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
250
		if ( false !== $repeat_value ) {
251 1
			if ( ! empty( $utc_values[ $utc_key ] ) ) {
252
				$this->_save_utc_value( $utc_key, $utc_values[ $utc_key ] );
253
				unset( $utc_values[ $utc_key ] );
254 1
			}
255
256 1
			return $repeat_value;
257 1
		}
258 1
259
		$tzstring = null;
260 1
261
		if ( is_array( $this->value ) && array_key_exists( 'timezone', $this->value ) ) {
262
			$tzstring = $this->value['timezone'];
263
		}
264 1
265
		if ( empty( $tzstring ) ) {
266 1
			$tzstring = cmb2_utils()->timezone_string();
267
		}
268
269
		$offset = cmb2_utils()->timezone_offset( $tzstring );
270
271
		if ( 'UTC' === substr( $tzstring, 0, 3 ) ) {
272
			$tzstring = timezone_name_from_abbr( '', $offset, 0 );
273
			/*
274
			 * timezone_name_from_abbr() returns false if not found based on offset.
275
			 * Since there are currently some invalid timezones in wp_timezone_dropdown(),
276
			 * fallback to an offset of 0 (UTC+0)
277 1
			 * https://core.trac.wordpress.org/ticket/29205
278 1
			 */
279
			$tzstring = false !== $tzstring ? $tzstring : timezone_name_from_abbr( '', 0, 0 );
280
		}
281
282 1
		$full_format = $this->field->args['date_format'] . ' ' . $this->field->args['time_format'];
283
		$full_date   = $this->value['date'] . ' ' . $this->value['time'];
284 1
285
		try {
286
287 1
			$datetime = date_create_from_format( $full_format, $full_date );
288 1
289 1
			if ( ! is_object( $datetime ) ) {
290
				$this->value = $utc_stamp = '';
291
			} else {
292 1
				$timestamp   = $datetime->setTimezone( new DateTimeZone( $tzstring ) )->getTimestamp();
293 1
				$utc_stamp   = $timestamp - $offset;
294 1
				$this->value = serialize( $datetime );
295 1
			}
296 1
297
			if ( $this->field->group ) {
298 1
				$this->value = array(
299
					'supporting_field_value' => $utc_stamp,
300
					'supporting_field_id'    => $utc_key,
301
					'value'                  => $this->value,
302
				);
303
			} else {
304
				// Save the utc timestamp supporting field
305
				if ( $repeat ) {
306
					$utc_values[ $utc_key ][] = $utc_stamp;
307 1
				} else {
308
					$this->_save_utc_value( $utc_key, $utc_stamp );
309
				}
310
			}
311
312 1
		} catch ( Exception $e ) {
313
			$this->value = '';
314
			cmb2_utils()->log_if_debug( __METHOD__, __LINE__, $e->getMessage() );
315
		}
316
317
		return $this->value;
318
	}
319
320 2
	/**
321 2
	 * Sanitize textareas and wysiwyg fields
322
	 * @since  1.0.1
323
	 * @return string       Sanitized data
324
	 */
325
	public function textarea() {
326
		return is_array( $this->value ) ? array_map( 'wp_kses_post', $this->value ) : wp_kses_post( $this->value );
327
	}
328
329
	/**
330
	 * Sanitize code textareas
331
	 * @since  1.0.2
332
	 * @return string       Sanitized data
333
	 */
334
	public function textarea_code( $repeat = false ) {
335
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
336
		if ( false !== $repeat_value ) {
337
			return $repeat_value;
338
		}
339
340
		return htmlspecialchars_decode( stripslashes( $this->value ) );
341
	}
342
343 1
	/**
344 1
	 * Validate a timezone against the available options.
345
	 * @since  2.1.3
346 1
	 * @return mixed  Timezone string or false if invalid.
347
	 */
348 1
	public function select_timezone() {
349 1
		// Validate against timezone list.
350
		$timezone = $this->validate_against_options( timezone_identifiers_list() );
351
		if ( $timezone !== false ) {
352
			return $timezone;
353
		}
354 1
355
		// Validate manual UTC offsets.
356
		if ( preg_match( '`^UTC[+-][0-9]+(?:\.(?:5|75))?$`', $this->value ) === 1 ) {
357
			return $this->value;
358
		}
359
360
		// Invalid.
361 1
		return false;
362 1
	}
363 1
364 1
	/**
365
	 * Handles saving of attachment post ID and sanitizing file url
366
	 * @since  1.1.0
367 1
	 * @return string        Sanitized url
368 1
	 */
369 1
	public function file() {
370
		$file_id_key = $this->field->_id() . '_id';
371
372 1
		if ( $this->field->group ) {
373 1
			// Return an array with url/id if saving a group field
374 1
			$this->value = $this->_get_group_file_value_array( $file_id_key );
375 1
		} else {
376
			$this->_save_file_id_value( $file_id_key );
377
			$this->text_url();
378
		}
379
380
		return $this->value;
381
	}
382
383
	/**
384
	 * Gets the values for the `file` field type from the data being saved.
385
	 * @since  2.2.0
386
	 */
387
	public function _get_group_file_value_array( $id_key ) {
388
		$alldata = $this->field->group->data_to_save;
389
		$base_id = $this->field->group->_id();
390
		$i       = $this->field->group->index;
391
392
		// Check group $alldata data
393
		$id_val  = isset( $alldata[ $base_id ][ $i ][ $id_key ] )
394
			? absint( $alldata[ $base_id ][ $i ][ $id_key ] )
395
			: 0;
396
397
		return array(
398
			'value' => $this->text_url(),
399
			'supporting_field_value' => $id_val,
400
			'supporting_field_id'    => $id_key,
401
		);
402
	}
403
404
	/**
405
	 * Peforms saving of `file` attachement's ID
406
	 * @since  1.1.0
407
	 */
408
	public function _save_file_id_value( $file_id_key ) {
409
		$id_field = $this->_new_supporting_field( $file_id_key );
410
411
		// Check standard data_to_save data
412
		$id_val = isset( $this->field->data_to_save[ $file_id_key ] )
413
			? $this->field->data_to_save[ $file_id_key ]
414
			: null;
415
416
		// If there is no ID saved yet, try to get it from the url
417
		if ( $this->value && ! $id_val ) {
418
			$id_val = cmb2_utils()->image_id_from_url( $this->value );
419
		}
420
421
		return $id_field->save_field( $id_val );
422
	}
423
424
	/**
425
	 * Peforms saving of `text_datetime_timestamp_timezone` utc timestamp
426
	 * @since  2.2.0
427
	 */
428
	public function _save_utc_value( $utc_key, $utc_stamp ) {
429
		return $this->_new_supporting_field( $utc_key )->save_field( $utc_stamp );
430
	}
431
432
	/**
433 1
	 * Returns a new, supporting, CMB2_Field object based on a new field id.
434 1
	 * @since  2.2.0
435 1
	 */
436
	public function _new_supporting_field( $new_field_id ) {
437
		$args = $this->field->args();
438 1
		unset( $args['_id'], $args['_name'] );
439
440 1
		$args['id'] = $new_field_id;
441 1
		$args['sanitization_cb'] = false;
442 1
443 1
		// And get new field object
444 1
		return new CMB2_Field( array(
445 1
			'field_args'  => $args,
446 1
			'group_field' => $this->field->group,
447 1
			'object_id'   => $this->field->object_id,
448 1
			'object_type' => $this->field->object_type,
449
		) );
450 1
	}
451
452 1
	/**
453
	 * If repeating, loop through and re-apply sanitization method
454
	 * @since  1.1.0
455
	 * @param  string $method Class method
456
	 * @param  bool   $repeat Whether repeating or not
457
	 * @return mixed          Sanitized value
458
	 */
459
	public function _check_repeat( $method, $repeat ) {
460
		if ( $repeat || ! $this->field->args( 'repeatable' ) ) {
461 8
			return false;
462 8
		}
463 3
464 3
		$values_array = $this->value;
465
466 6
		$new_value = array();
467
		foreach ( $values_array as $iterator => $this->value ) {
468
			if ( $this->value ) {
469
				$val = $this->$method( true );
470
				if ( ! empty( $val ) ) {
471
					$new_value[] = $val;
472
				}
473
			}
474
		}
475
476
		$this->value = $new_value;
477
478
		return empty( $this->value ) ? null : $this->value;
479
	}
480
481
	/**
482
	 * Determine if passed value is an empty array
483
	 * @since  2.0.6
484
	 * @param  mixed  $to_check Value to check
485
	 * @return boolean          Whether value is an array that's empty
486
	 */
487
	public function _is_empty_array( $to_check ) {
488
		if ( is_array( $to_check ) ) {
489
			$cleaned_up = array_filter( $to_check );
490
			return empty( $cleaned_up );
491
		}
492
		return false;
493
	}
494
495
	/**
496
	 * Validate a value against a set of allowed options.
497
	 *
498
	 * @since  2.1.3
499
	 * @param array    Options to validate against. Defaults to the `options` field argument.
500
	 * @return mixed   Validated option or false if the value is invalid.
501
	 *                 The validated option may be empty if `show_option_none` was set to true.
502
	 */
503
	protected function validate_against_options( $options = array() ) {
504
		if ( '' === $this->value ) {
505
			if ( true === $this->field->args( 'show_option_none' ) ) {
506
				return $this->value;
507
			} else {
508
				return false;
509
			}
510
		}
511
512
		if ( empty( $options ) ) {
513
			$options = $this->field->options();
514
			$options = array_keys( $options );
515
		}
516
517
		/**
518
		 * Allow for dynamically altering the options array used for validation.
519
		 *
520
		 * The dynamic portion of the hook name, $this->field->type(), is the field type
521
		 * of the current object. This will either be 'radio', 'radio_inline', 'select'
522
		 * or 'select_timezone'.
523
		 *
524
		 * @param array  $options     The options for the field.
525
		 * @param int    $object_id   The ID of the current object
526
		 */
527
		$options = apply_filters( "cmb2_validate_{$this->field->type()}_options", $options, $this->field->object_id );
528
529
		if ( in_array( $this->value, $options, true ) ) {
530
			return $this->value;
531
		}
532
533
		return false;
534
	}
535
536
}
537