Completed
Pull Request — trunk (#541)
by Justin
40:31 queued 37:55
created

CMB2_Sanitize::_new_supporting_field()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 9.4285
ccs 5
cts 5
cp 1
crap 1
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 20
	public function __construct( CMB2_Field $field, $value ) {
36 20
		$this->field = $field;
37 20
		$this->value = stripslashes_deep( $value ); // get rid of those evil magic quotes
38 20
	}
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 17
	public function __call( $name, $arguments ) {
47 17
		return $this->default_sanitization();
48
	}
49
50
	/**
51
	 * Default fallback sanitization method. Applies filters.
52
	 * @since  1.0.2
53
	 */
54 17
	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 17
		if ( function_exists( 'apply_filters_deprecated' ) ) {
62 17
			$override_value = apply_filters_deprecated( "cmb2_validate_{$this->field->type()}", array( null, $this->value, $this->field->object_id, $this->field->args(), $this ), '2.0.0', "cmb2_sanitize_{$this->field->type()}" );
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...
63 17
		} else {
64
			$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...
65
		}
66
67 17
		if ( null !== $override_value ) {
68
			return $override_value;
69
		}
70
71 17
		$sanitized_value = '';
72 17
		switch ( $this->field->type() ) {
73 17
			case 'wysiwyg':
74 17
			case 'textarea_small':
75 17
			case 'oembed':
76 2
				$sanitized_value = $this->textarea();
77 2
				break;
78 17
			case 'taxonomy_select':
79 17
			case 'taxonomy_radio':
80 17
			case 'taxonomy_radio_inline':
81 17
			case 'taxonomy_multicheck':
82 17
			case 'taxonomy_multicheck_inline':
83
				if ( $this->field->args( 'taxonomy' ) ) {
84
					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...
85
				} else {
86
					CMB2_Utils::log_if_debug( __METHOD__, __LINE__, "{$this->field->type()} {$this->field->_id()} is missing the 'taxonomy' parameter." );
87
				}
88
				break;
89 17
			case 'multicheck':
90 17
			case 'multicheck_inline':
91 17
			case 'file_list':
92 17
			case 'group':
93
				// no filtering
94 2
				$sanitized_value = $this->value;
95 2
				break;
96 16
			default:
97
				// Handle repeatable fields array
98
				// We'll fallback to 'sanitize_text_field'
99 16
				$sanitized_value = is_array( $this->value ) ? array_map( 'sanitize_text_field', $this->value ) : sanitize_text_field( $this->value );
100 16
				break;
101 17
		}
102
103 17
		return $this->_is_empty_array( $sanitized_value ) ? '' : $sanitized_value;
104
	}
105
106
	/**
107
	 * Simple checkbox validation
108
	 * @since  1.0.1
109
	 * @return string|false 'on' or false
110
	 */
111
	public function checkbox() {
112
		return $this->value === 'on' ? 'on' : false;
113
	}
114
115
	/**
116
	 * Validate url in a meta value
117
	 * @since  1.0.1
118
	 * @return string        Empty string or escaped url
119
	 */
120 1
	public function text_url() {
121 1
		$protocols = $this->field->args( 'protocols' );
122
		// for repeatable
123 1
		if ( is_array( $this->value ) ) {
124
			foreach ( $this->value as $key => $val ) {
125
				$this->value[ $key ] = $val ? esc_url_raw( $val, $protocols ) : $this->field->get_default();
126
			}
127
		} else {
128 1
			$this->value = $this->value ? esc_url_raw( $this->value, $protocols ) : $this->field->get_default();
129
		}
130
131 1
		return $this->value;
132
	}
133
134
	public function colorpicker() {
135
		// for repeatable
136
		if ( is_array( $this->value ) ) {
137
			$check = $this->value;
138
			$this->value = array();
139
			foreach ( $check as $key => $val ) {
140
				if ( $val && '#' != $val ) {
141
					$this->value[ $key ] = esc_attr( $val );
142
				}
143
			}
144
		} else {
145
			$this->value = ! $this->value || '#' == $this->value ? '' : esc_attr( $this->value );
146
		}
147
		return $this->value;
148
	}
149
150
	/**
151
	 * Validate email in a meta value
152
	 * @since  1.0.1
153
	 * @return string       Empty string or sanitized email
154
	 */
155
	public function text_email() {
156
		// for repeatable
157
		if ( is_array( $this->value ) ) {
158
			foreach ( $this->value as $key => $val ) {
159
				$val = trim( $val );
160
				$this->value[ $key ] = is_email( $val ) ? $val : '';
161
			}
162
		} else {
163
			$this->value = trim( $this->value );
164
			$this->value = is_email( $this->value ) ? $this->value : '';
165
		}
166
167
		return $this->value;
168
	}
169
170
	/**
171
	 * Validate money in a meta value
172
	 * @since  1.0.1
173
	 * @return string Empty string or sanitized money value
174
	 */
175 1
	public function text_money() {
176 1
		if ( ! $this->value ) {
177 1
			return '';
178
		}
179
180 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...
181
182 1
		$search = array( $wp_locale->number_format['thousands_sep'], $wp_locale->number_format['decimal_point'] );
183 1
		$replace = array( '', '.' );
184
185
		// for repeatable
186 1
		if ( is_array( $this->value ) ) {
187
			foreach ( $this->value as $key => $val ) {
188
				if ( $val ) {
189
					$this->value[ $key ] = number_format_i18n( (float) str_ireplace( $search, $replace, $val ), 2 );
190
				}
191
			}
192
		} else {
193 1
			$this->value = number_format_i18n( (float) str_ireplace( $search, $replace, $this->value ), 2 );
194
		}
195
196 1
		return $this->value;
197
	}
198
199
	/**
200
	 * Converts text date to timestamp
201
	 * @since  1.0.2
202
	 * @return string Timestring
203
	 */
204
	public function text_date_timestamp() {
205
		return is_array( $this->value )
206
			? array_map( array( $this->field, 'get_timestamp_from_value' ), $this->value )
207
			: $this->field->get_timestamp_from_value( $this->value );
208
	}
209
210
	/**
211
	 * Datetime to timestamp
212
	 * @since  1.0.1
213
	 * @return string Timestring
214
	 */
215
	public function text_datetime_timestamp( $repeat = false ) {
216
217
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
218
		if ( empty( $test ) ) {
219
			return '';
220
		}
221
222
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
223
		if ( false !== $repeat_value ) {
224
			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...
225
		}
226
227
		if ( isset( $this->value['date'], $this->value['time'] ) ) {
228
			$this->value = $this->field->get_timestamp_from_value( $this->value['date'] . ' ' . $this->value['time'] );
229
		}
230
231
		if ( $tz_offset = $this->field->field_timezone_offset() ) {
232
			$this->value += (int) $tz_offset;
233
		}
234
235
		return $this->value;
236
	}
237
238
	/**
239
	 * Datetime to timestamp with timezone
240
	 * @since  1.0.1
241
	 * @return string       Timestring
242
	 */
243 2
	public function text_datetime_timestamp_timezone( $repeat = false ) {
244 2
		static $utc_values = array();
245
246 2
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
247 2
		if ( empty( $test ) ) {
248 1
			return '';
249
		}
250
251 2
		$utc_key = $this->field->_id() . '_utc';
252
253 2
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
254 2
		if ( false !== $repeat_value ) {
255 1
			if ( ! empty( $utc_values[ $utc_key ] ) ) {
256
				$this->_save_utc_value( $utc_key, $utc_values[ $utc_key ] );
257
				unset( $utc_values[ $utc_key ] );
258
			}
259
260 1
			return $repeat_value;
261
		}
262
263 2
		$tzstring = null;
264
265 2
		if ( is_array( $this->value ) && array_key_exists( 'timezone', $this->value ) ) {
266 2
			$tzstring = $this->value['timezone'];
267 2
		}
268
269 2
		if ( empty( $tzstring ) ) {
270
			$tzstring = CMB2_Utils::timezone_string();
271
		}
272
273 2
		$offset = CMB2_Utils::timezone_offset( $tzstring );
274
275 2
		if ( 'UTC' === substr( $tzstring, 0, 3 ) ) {
276 1
			$tzstring = timezone_name_from_abbr( '', $offset, 0 );
277
			/*
278
			 * timezone_name_from_abbr() returns false if not found based on offset.
279
			 * Since there are currently some invalid timezones in wp_timezone_dropdown(),
280
			 * fallback to an offset of 0 (UTC+0)
281
			 * https://core.trac.wordpress.org/ticket/29205
282
			 */
283 1
			$tzstring = false !== $tzstring ? $tzstring : timezone_name_from_abbr( '', 0, 0 );
284 1
		}
285
286 2
		$full_format = $this->field->args['date_format'] . ' ' . $this->field->args['time_format'];
287 2
		$full_date   = $this->value['date'] . ' ' . $this->value['time'];
288
289
		try {
290
291 2
			$datetime = date_create_from_format( $full_format, $full_date );
292
293 2
			if ( ! is_object( $datetime ) ) {
294
				$this->value = $utc_stamp = '';
295
			} else {
296 2
				$timestamp   = $datetime->setTimezone( new DateTimeZone( $tzstring ) )->getTimestamp();
297 2
				$utc_stamp   = $timestamp - $offset;
298 2
				$this->value = serialize( $datetime );
299
			}
300
301 2
			if ( $this->field->group ) {
302 1
				$this->value = array(
303 1
					'supporting_field_value' => $utc_stamp,
304 1
					'supporting_field_id'    => $utc_key,
305 1
					'value'                  => $this->value,
306
				);
307 1
			} else {
308
				// Save the utc timestamp supporting field
309 1
				if ( $repeat ) {
310
					$utc_values[ $utc_key ][] = $utc_stamp;
311
				} else {
312 1
					$this->_save_utc_value( $utc_key, $utc_stamp );
313
				}
314
			}
315
316 2
		} catch ( Exception $e ) {
317
			$this->value = '';
318
			CMB2_Utils::log_if_debug( __METHOD__, __LINE__, $e->getMessage() );
319
		}
320
321 2
		return $this->value;
322
	}
323
324
	/**
325
	 * Sanitize textareas and wysiwyg fields
326
	 * @since  1.0.1
327
	 * @return string       Sanitized data
328
	 */
329 2
	public function textarea() {
330 2
		return is_array( $this->value ) ? array_map( 'wp_kses_post', $this->value ) : wp_kses_post( $this->value );
331
	}
332
333
	/**
334
	 * Sanitize code textareas
335
	 * @since  1.0.2
336
	 * @return string       Sanitized data
337
	 */
338
	public function textarea_code( $repeat = false ) {
339
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
340
		if ( false !== $repeat_value ) {
341
			return $repeat_value;
342
		}
343
344
		return htmlspecialchars_decode( stripslashes( $this->value ) );
345
	}
346
347
	/**
348
	 * Handles saving of attachment post ID and sanitizing file url
349
	 * @since  1.1.0
350
	 * @return string        Sanitized url
351
	 */
352 1
	public function file() {
353 1
		$file_id_key = $this->field->_id() . '_id';
354
355 1
		if ( $this->field->group ) {
356
			// Return an array with url/id if saving a group field
357 1
			$this->value = $this->_get_group_file_value_array( $file_id_key );
358 1
		} else {
359
			$this->_save_file_id_value( $file_id_key );
360
			$this->text_url();
361
		}
362
363 1
		return $this->value;
364
	}
365
366
	/**
367
	 * Gets the values for the `file` field type from the data being saved.
368
	 * @since  2.2.0
369
	 */
370 1
	public function _get_group_file_value_array( $id_key ) {
371 1
		$alldata = $this->field->group->data_to_save;
372 1
		$base_id = $this->field->group->_id();
373 1
		$i       = $this->field->group->index;
374
375
		// Check group $alldata data
376 1
		$id_val  = isset( $alldata[ $base_id ][ $i ][ $id_key ] )
377 1
			? absint( $alldata[ $base_id ][ $i ][ $id_key ] )
378 1
			: 0;
379
380
		return array(
381 1
			'value' => $this->text_url(),
382 1
			'supporting_field_value' => $id_val,
383 1
			'supporting_field_id'    => $id_key,
384 1
		);
385
	}
386
387
	/**
388
	 * Peforms saving of `file` attachement's ID
389
	 * @since  1.1.0
390
	 */
391
	public function _save_file_id_value( $file_id_key ) {
392
		$id_field = $this->_new_supporting_field( $file_id_key );
393
394
		// Check standard data_to_save data
395
		$id_val = isset( $this->field->data_to_save[ $file_id_key ] )
396
			? $this->field->data_to_save[ $file_id_key ]
397
			: null;
398
399
		// If there is no ID saved yet, try to get it from the url
400
		if ( $this->value && ! $id_val ) {
401
			$id_val = CMB2_Utils::image_id_from_url( $this->value );
402
		}
403
404
		return $id_field->save_field( $id_val );
405
	}
406
407
	/**
408
	 * Peforms saving of `text_datetime_timestamp_timezone` utc timestamp
409
	 * @since  2.2.0
410
	 */
411 1
	public function _save_utc_value( $utc_key, $utc_stamp ) {
412 1
		return $this->_new_supporting_field( $utc_key )->save_field( $utc_stamp );
413
	}
414
415
	/**
416
	 * Returns a new, supporting, CMB2_Field object based on a new field id.
417
	 * @since  2.2.0
418
	 */
419 1
	public function _new_supporting_field( $new_field_id ) {
420 1
		return $this->field->get_field_clone( array(
421 1
			'id' => $new_field_id,
422 1
			'sanitization_cb' => false,
423 1
		) );
424
	}
425
426
	/**
427
	 * If repeating, loop through and re-apply sanitization method
428
	 * @since  1.1.0
429
	 * @param  string $method Class method
430
	 * @param  bool   $repeat Whether repeating or not
431
	 * @return mixed          Sanitized value
432
	 */
433 2
	public function _check_repeat( $method, $repeat ) {
434 2
		if ( $repeat || ! $this->field->args( 'repeatable' ) ) {
435 2
			return false;
436
		}
437
438 1
		$values_array = $this->value;
439
440 1
		$new_value = array();
441 1
		foreach ( $values_array as $iterator => $this->value ) {
442 1
			if ( $this->value ) {
443 1
				$val = $this->$method( true );
444 1
				if ( ! empty( $val ) ) {
445 1
					$new_value[] = $val;
446 1
				}
447 1
			}
448 1
		}
449
450 1
		$this->value = $new_value;
451
452 1
		return empty( $this->value ) ? null : $this->value;
453
	}
454
455
	/**
456
	 * Determine if passed value is an empty array
457
	 * @since  2.0.6
458
	 * @param  mixed  $to_check Value to check
459
	 * @return boolean          Whether value is an array that's empty
460
	 */
461 17
	public function _is_empty_array( $to_check ) {
462 17
		if ( is_array( $to_check ) ) {
463 3
			$cleaned_up = array_filter( $to_check );
464 3
			return empty( $cleaned_up );
465
		}
466 15
		return false;
467
	}
468
469
}
470