Completed
Pull Request — trunk (#541)
by Justin
12:21 queued 08:58
created

CMB2_Sanitize::default_sanitization()   D

Complexity

Conditions 17
Paths 39

Size

Total Lines 47
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 18.905

Importance

Changes 0
Metric Value
cc 17
eloc 31
c 0
b 0
f 0
nc 39
nop 0
dl 0
loc 47
ccs 26
cts 32
cp 0.8125
crap 18.905
rs 4.9547

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 13
	public function __construct( CMB2_Field $field, $value ) {
36 13
		$this->field = $field;
37 13
		$this->value = stripslashes_deep( $value ); // get rid of those evil magic quotes
38 13
	}
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 10
	public function __call( $name, $arguments ) {
47 10
		return $this->default_sanitization();
48
	}
49
50
	/**
51
	 * Default fallback sanitization method. Applies filters.
52
	 * @since  1.0.2
53
	 */
54 10
	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 10
		$override_value = apply_filters( "cmb2_validate_{$this->field->type()}", null, $this->value, $this->field->object_id, $this->field->args(), $this );
0 ignored issues
show
Documentation introduced by
The property $object_id is declared protected in CMB2_Base. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
62
63 10
		if ( null !== $override_value ) {
64
			return $override_value;
65
		}
66
67 10
		$sanitized_value = '';
68 10
		switch ( $this->field->type() ) {
69 10
			case 'wysiwyg':
70 10
			case 'textarea_small':
71 10
			case 'oembed':
72 1
				$sanitized_value = $this->textarea();
73 1
				break;
74 10
			case 'taxonomy_select':
75 10
			case 'taxonomy_radio':
76 10
			case 'taxonomy_radio_inline':
77 10
			case 'taxonomy_multicheck':
78 10
			case 'taxonomy_multicheck_inline':
79
				if ( $this->field->args( 'taxonomy' ) ) {
80
					wp_set_object_terms( $this->field->object_id, $this->value, $this->field->args( 'taxonomy' ) );
0 ignored issues
show
Documentation introduced by
The property $object_id is declared protected in CMB2_Base. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
81
				} else {
82
					CMB2_Utils::log_if_debug( __METHOD__, __LINE__, "{$this->field->type()} {$this->field->_id()} is missing the 'taxonomy' parameter." );
83
				}
84
				break;
85 10
			case 'multicheck':
86 10
			case 'multicheck_inline':
87 10
			case 'file_list':
88 10
			case 'group':
89
				// no filtering
90 2
				$sanitized_value = $this->value;
91 2
				break;
92 9
			default:
93
				// Handle repeatable fields array
94
				// We'll fallback to 'sanitize_text_field'
95 9
				$sanitized_value = is_array( $this->value ) ? array_map( 'sanitize_text_field', $this->value ) : sanitize_text_field( $this->value );
96 9
				break;
97 10
		}
98
99 10
		return $this->_is_empty_array( $sanitized_value ) ? '' : $sanitized_value;
100
	}
101
102
	/**
103
	 * Simple checkbox validation
104
	 * @since  1.0.1
105
	 * @return string|false 'on' or false
106
	 */
107
	public function checkbox() {
108
		return $this->value === 'on' ? 'on' : false;
109
	}
110
111
	/**
112
	 * Validate url in a meta value
113
	 * @since  1.0.1
114
	 * @return string        Empty string or escaped url
115
	 */
116
	public function text_url() {
117
		$protocols = $this->field->args( 'protocols' );
118
		// for repeatable
119
		if ( is_array( $this->value ) ) {
120
			foreach ( $this->value as $key => $val ) {
121
				$this->value[ $key ] = $val ? esc_url_raw( $val, $protocols ) : $this->field->get_default();
122
			}
123
		} else {
124
			$this->value = $this->value ? esc_url_raw( $this->value, $protocols ) : $this->field->get_default();
125
		}
126
127
		return $this->value;
128
	}
129
130
	public function colorpicker() {
131
		// for repeatable
132
		if ( is_array( $this->value ) ) {
133
			$check = $this->value;
134
			$this->value = array();
135
			foreach ( $check as $key => $val ) {
136
				if ( $val && '#' != $val ) {
137
					$this->value[ $key ] = esc_attr( $val );
138
				}
139
			}
140
		} else {
141
			$this->value = ! $this->value || '#' == $this->value ? '' : esc_attr( $this->value );
142
		}
143
		return $this->value;
144
	}
145
146
	/**
147
	 * Validate email in a meta value
148
	 * @since  1.0.1
149
	 * @return string       Empty string or sanitized email
150
	 */
151
	public function text_email() {
152
		// for repeatable
153
		if ( is_array( $this->value ) ) {
154
			foreach ( $this->value as $key => $val ) {
155
				$val = trim( $val );
156
				$this->value[ $key ] = is_email( $val ) ? $val : '';
157
			}
158
		} else {
159
			$this->value = trim( $this->value );
160
			$this->value = is_email( $this->value ) ? $this->value : '';
161
		}
162
163
		return $this->value;
164
	}
165
166
	/**
167
	 * Validate money in a meta value
168
	 * @since  1.0.1
169
	 * @return string Empty string or sanitized money value
170
	 */
171 1
	public function text_money() {
172 1
		if ( ! $this->value ) {
173 1
			return '';
174
		}
175
176 1
		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...
177
178 1
		$search = array( $wp_locale->number_format['thousands_sep'], $wp_locale->number_format['decimal_point'] );
179 1
		$replace = array( '', '.' );
180
181
		// for repeatable
182 1
		if ( is_array( $this->value ) ) {
183
			foreach ( $this->value as $key => $val ) {
184
				if ( $val ) {
185
					$this->value[ $key ] = number_format_i18n( (float) str_ireplace( $search, $replace, $val ), 2 );
186
				}
187
			}
188
		} else {
189 1
			$this->value = number_format_i18n( (float) str_ireplace( $search, $replace, $this->value ), 2 );
190
		}
191
192 1
		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
	/**
235
	 * Datetime to timestamp with timezone
236
	 * @since  1.0.1
237
	 * @return string       Timestring
238
	 */
239 1
	public function text_datetime_timestamp_timezone( $repeat = false ) {
240 1
		static $utc_values = array();
241
242 1
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
243 1
		if ( empty( $test ) ) {
244
			return '';
245
		}
246
247 1
		$utc_key = $this->field->_id() . '_utc';
248
249 1
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
250 1
		if ( false !== $repeat_value ) {
251
			if ( ! empty( $utc_values[ $utc_key ] ) ) {
252
				$this->_save_utc_value( $utc_key, $utc_values[ $utc_key ] );
253
				unset( $utc_values[ $utc_key ] );
254
			}
255
256
			return $repeat_value;
257
		}
258
259 1
		$tzstring = null;
260
261 1
		if ( is_array( $this->value ) && array_key_exists( 'timezone', $this->value ) ) {
262 1
			$tzstring = $this->value['timezone'];
263 1
		}
264
265 1
		if ( empty( $tzstring ) ) {
266
			$tzstring = CMB2_Utils::timezone_string();
267
		}
268
269 1
		$offset = CMB2_Utils::timezone_offset( $tzstring );
270
271 1
		if ( 'UTC' === substr( $tzstring, 0, 3 ) ) {
272 1
			$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
			 * https://core.trac.wordpress.org/ticket/29205
278
			 */
279 1
			$tzstring = false !== $tzstring ? $tzstring : timezone_name_from_abbr( '', 0, 0 );
280 1
		}
281
282 1
		$full_format = $this->field->args['date_format'] . ' ' . $this->field->args['time_format'];
283 1
		$full_date   = $this->value['date'] . ' ' . $this->value['time'];
284
285
		try {
286
287 1
			$datetime = date_create_from_format( $full_format, $full_date );
288
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
			}
296
297 1
			if ( $this->field->group ) {
298
				$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 1
				if ( $repeat ) {
306
					$utc_values[ $utc_key ][] = $utc_stamp;
307
				} else {
308 1
					$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 1
		return $this->value;
318
	}
319
320
	/**
321
	 * Sanitize textareas and wysiwyg fields
322
	 * @since  1.0.1
323
	 * @return string       Sanitized data
324
	 */
325 1
	public function textarea() {
326 1
		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
	/**
344
	 * Handles saving of attachment post ID and sanitizing file url
345
	 * @since  1.1.0
346
	 * @return string        Sanitized url
347
	 */
348
	public function file() {
349
		$file_id_key = $this->field->_id() . '_id';
350
351
		if ( $this->field->group ) {
352
			// Return an array with url/id if saving a group field
353
			$this->value = $this->_get_group_file_value_array( $file_id_key );
354
		} else {
355
			$this->_save_file_id_value( $file_id_key );
356
			$this->text_url();
357
		}
358
359
		return $this->value;
360
	}
361
362
	/**
363
	 * Gets the values for the `file` field type from the data being saved.
364
	 * @since  2.2.0
365
	 */
366
	public function _get_group_file_value_array( $id_key ) {
367
		$alldata = $this->field->group->data_to_save;
368
		$base_id = $this->field->group->_id();
369
		$i       = $this->field->group->index;
370
371
		// Check group $alldata data
372
		$id_val  = isset( $alldata[ $base_id ][ $i ][ $id_key ] )
373
			? absint( $alldata[ $base_id ][ $i ][ $id_key ] )
374
			: 0;
375
376
		return array(
377
			'value' => $this->text_url(),
378
			'supporting_field_value' => $id_val,
379
			'supporting_field_id'    => $id_key,
380
		);
381
	}
382
383
	/**
384
	 * Peforms saving of `file` attachement's ID
385
	 * @since  1.1.0
386
	 */
387
	public function _save_file_id_value( $file_id_key ) {
388
		$id_field = $this->_new_supporting_field( $file_id_key );
389
390
		// Check standard data_to_save data
391
		$id_val = isset( $this->field->data_to_save[ $file_id_key ] )
392
			? $this->field->data_to_save[ $file_id_key ]
393
			: null;
394
395
		// If there is no ID saved yet, try to get it from the url
396
		if ( $this->value && ! $id_val ) {
397
			$id_val = CMB2_Utils::image_id_from_url( $this->value );
398
		}
399
400
		return $id_field->save_field( $id_val );
401
	}
402
403
	/**
404
	 * Peforms saving of `text_datetime_timestamp_timezone` utc timestamp
405
	 * @since  2.2.0
406
	 */
407 1
	public function _save_utc_value( $utc_key, $utc_stamp ) {
408 1
		return $this->_new_supporting_field( $utc_key )->save_field( $utc_stamp );
409
	}
410
411
	/**
412
	 * Returns a new, supporting, CMB2_Field object based on a new field id.
413
	 * @since  2.2.0
414
	 */
415 1
	public function _new_supporting_field( $new_field_id ) {
416 1
		return $this->field->get_field_clone( array(
417 1
			'id' => $new_field_id,
418 1
			'sanitization_cb' => false,
419 1
		) );
420
	}
421
422
	/**
423
	 * If repeating, loop through and re-apply sanitization method
424
	 * @since  1.1.0
425
	 * @param  string $method Class method
426
	 * @param  bool   $repeat Whether repeating or not
427
	 * @return mixed          Sanitized value
428
	 */
429 1
	public function _check_repeat( $method, $repeat ) {
430 1
		if ( $repeat || ! $this->field->args( 'repeatable' ) ) {
431 1
			return false;
432
		}
433
434
		$values_array = $this->value;
435
436
		$new_value = array();
437
		foreach ( $values_array as $iterator => $this->value ) {
438
			if ( $this->value ) {
439
				$val = $this->$method( true );
440
				if ( ! empty( $val ) ) {
441
					$new_value[] = $val;
442
				}
443
			}
444
		}
445
446
		$this->value = $new_value;
447
448
		return empty( $this->value ) ? null : $this->value;
449
	}
450
451
	/**
452
	 * Determine if passed value is an empty array
453
	 * @since  2.0.6
454
	 * @param  mixed  $to_check Value to check
455
	 * @return boolean          Whether value is an array that's empty
456
	 */
457 10
	public function _is_empty_array( $to_check ) {
458 10
		if ( is_array( $to_check ) ) {
459 3
			$cleaned_up = array_filter( $to_check );
460 3
			return empty( $cleaned_up );
461
		}
462 8
		return false;
463
	}
464
465
}
466