Completed
Pull Request — trunk (#577)
by Juliette
11:53
created

CMB2_Sanitize::text_datetime_timestamp()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42
Metric Value
dl 0
loc 22
ccs 0
cts 10
cp 0
rs 8.6737
cc 6
eloc 12
nc 12
nop 1
crap 42
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 'taxonomy_select':
75 8
			case 'taxonomy_radio':
76 8
			case 'taxonomy_radio_inline':
77 8
			case 'taxonomy_multicheck':
78 8
			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' ) );
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 8
			case 'multicheck':
86 8
			case 'multicheck_inline':
87 8
			case 'file_list':
88 8
			case 'group':
89
				// no filtering
90 2
				$sanitized_value = $this->value;
91 2
				break;
92 7
			default:
93
				// Handle repeatable fields array
94
				// We'll fallback to 'sanitize_text_field'
95 7
				$sanitized_value = is_array( $this->value ) ? array_map( 'sanitize_text_field', $this->value ) : sanitize_text_field( $this->value );
96 7
				break;
97 8
		}
98
99 8
		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 1
	public function text_url() {
117 1
		$protocols = $this->field->args( 'protocols' );
118
		// for repeatable
119 1
		if ( is_array( $this->value ) ) {
120
			foreach ( $this->value as $key => $val ) {
121
				$this->value[ $key ] = $val ? esc_url_raw( $val, $protocols ) : $this->field->args( 'default' );
122
			}
123
		} else {
124 1
			$this->value = $this->value ? esc_url_raw( $this->value, $protocols ) : $this->field->args( 'default' );
125
		}
126
127 1
		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
	public function text_money() {
172
		if ( '' === $this->value ) {
173
			return $this->value;
174
		}
175
176
		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
		$search = array( $wp_locale->number_format['thousands_sep'], $wp_locale->number_format['decimal_point'] );
179
		$replace = array( '', '.' );
180
181
		// for repeatable
182
		if ( is_array( $this->value ) ) {
183
			foreach ( $this->value as $key => $val ) {
184
				if ( '' === $val ) {
185
					continue;
186
				}
187
				$this->value[ $key ] = number_format_i18n( (float) str_ireplace( $search, $replace, $val ), 2 );
188
			}
189
		} else {
190
			$this->value = number_format_i18n( (float) str_ireplace( $search, $replace, $this->value ), 2 );
191
		}
192
193
		return $this->value;
194
	}
195
196
	/**
197
	 * Converts text date to timestamp
198
	 * @since  1.0.2
199
	 * @return string Timestring
200
	 */
201
	public function text_date_timestamp() {
202
		return is_array( $this->value )
203
			? array_map( array( $this->field, 'get_timestamp_from_value' ), $this->value )
204
			: $this->field->get_timestamp_from_value( $this->value );
205
	}
206
207
	/**
208
	 * Datetime to timestamp
209
	 * @since  1.0.1
210
	 * @return string Timestring
211
	 */
212
	public function text_datetime_timestamp( $repeat = false ) {
213
214
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
215
		if ( empty( $test ) ) {
216
			return '';
217
		}
218
219
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
220
		if ( false !== $repeat_value ) {
221
			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...
222
		}
223
224
		if ( isset( $this->value['date'], $this->value['time'] ) ) {
225
			$this->value = $this->field->get_timestamp_from_value( $this->value['date'] . ' ' . $this->value['time'] );
226
		}
227
228
		if ( $tz_offset = $this->field->field_timezone_offset() ) {
229
			$this->value += (int) $tz_offset;
230
		}
231
232
		return $this->value;
233
	}
234 1
235 1
	/**
236
	 * Datetime to timestamp with timezone
237 1
	 * @since  1.0.1
238 1
	 * @return string       Timestring
239 1
	 */
240
	public function text_datetime_timestamp_timezone( $repeat = false ) {
241
		static $utc_values = array();
242 1
243
		$test = is_array( $this->value ) ? array_filter( $this->value ) : '';
244 1
		if ( empty( $test ) ) {
245 1
			return '';
246 1
		}
247
248
		$utc_key = $this->field->_id() . '_utc';
249
250
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
251 1
		if ( false !== $repeat_value ) {
252
			if ( ! empty( $utc_values[ $utc_key ] ) ) {
253
				$this->_save_utc_value( $utc_key, $utc_values[ $utc_key ] );
254 1
				unset( $utc_values[ $utc_key ] );
255
			}
256 1
257 1
			return $repeat_value;
258 1
		}
259
260 1
		$tzstring = null;
261
262
		if ( is_array( $this->value ) && array_key_exists( 'timezone', $this->value ) ) {
263
			$tzstring = $this->value['timezone'];
264 1
		}
265
266 1
		if ( empty( $tzstring ) ) {
267
			$tzstring = cmb2_utils()->timezone_string();
268
		}
269
270
		$offset = cmb2_utils()->timezone_offset( $tzstring );
271
272
		if ( 'UTC' === substr( $tzstring, 0, 3 ) ) {
273
			$tzstring = timezone_name_from_abbr( '', $offset, 0 );
274
			/*
275
			 * timezone_name_from_abbr() returns false if not found based on offset.
276
			 * Since there are currently some invalid timezones in wp_timezone_dropdown(),
277 1
			 * fallback to an offset of 0 (UTC+0)
278 1
			 * https://core.trac.wordpress.org/ticket/29205
279
			 */
280
			$tzstring = false !== $tzstring ? $tzstring : timezone_name_from_abbr( '', 0, 0 );
281
		}
282 1
283
		$full_format = $this->field->args['date_format'] . ' ' . $this->field->args['time_format'];
284 1
		$full_date   = $this->value['date'] . ' ' . $this->value['time'];
285
286
		try {
287 1
288 1
			$datetime = date_create_from_format( $full_format, $full_date );
289 1
290
			if ( ! is_object( $datetime ) ) {
291
				$this->value = $utc_stamp = '';
292 1
			} else {
293 1
				$timestamp   = $datetime->setTimezone( new DateTimeZone( $tzstring ) )->getTimestamp();
294 1
				$utc_stamp   = $timestamp - $offset;
295 1
				$this->value = serialize( $datetime );
296 1
			}
297
298 1
			if ( $this->field->group ) {
299
				$this->value = array(
300
					'supporting_field_value' => $utc_stamp,
301
					'supporting_field_id'    => $utc_key,
302
					'value'                  => $this->value,
303
				);
304
			} else {
305
				// Save the utc timestamp supporting field
306
				if ( $repeat ) {
307 1
					$utc_values[ $utc_key ][] = $utc_stamp;
308
				} else {
309
					$this->_save_utc_value( $utc_key, $utc_stamp );
310
				}
311
			}
312 1
313
		} catch ( Exception $e ) {
314
			$this->value = '';
315
			cmb2_utils()->log_if_debug( __METHOD__, __LINE__, $e->getMessage() );
316
		}
317
318
		return $this->value;
319
	}
320 2
321 2
	/**
322
	 * Sanitize textareas and wysiwyg fields
323
	 * @since  1.0.1
324
	 * @return string       Sanitized data
325
	 */
326
	public function textarea() {
327
		return is_array( $this->value ) ? array_map( 'wp_kses_post', $this->value ) : wp_kses_post( $this->value );
328
	}
329
330
	/**
331
	 * Sanitize code textareas
332
	 * @since  1.0.2
333
	 * @return string       Sanitized data
334
	 */
335
	public function textarea_code( $repeat = false ) {
336
		$repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
337
		if ( false !== $repeat_value ) {
338
			return $repeat_value;
339
		}
340
341
		return htmlspecialchars_decode( stripslashes( $this->value ) );
342
	}
343 1
344 1
	/**
345
	 * Handles saving of attachment post ID and sanitizing file url
346 1
	 * @since  1.1.0
347
	 * @return string        Sanitized url
348 1
	 */
349 1
	public function file() {
350
		$file_id_key = $this->field->_id() . '_id';
351
352
		if ( $this->field->group ) {
353
			// Return an array with url/id if saving a group field
354 1
			$this->value = $this->_get_group_file_value_array( $file_id_key );
355
		} else {
356
			$this->_save_file_id_value( $file_id_key );
357
			$this->text_url();
358
		}
359
360
		return $this->value;
361 1
	}
362 1
363 1
	/**
364 1
	 * Gets the values for the `file` field type from the data being saved.
365
	 * @since  2.2.0
366
	 */
367 1
	public function _get_group_file_value_array( $id_key ) {
368 1
		$alldata = $this->field->group->data_to_save;
369 1
		$base_id = $this->field->group->_id();
370
		$i       = $this->field->group->index;
371
372 1
		// Check group $alldata data
373 1
		$id_val  = isset( $alldata[ $base_id ][ $i ][ $id_key ] )
374 1
			? absint( $alldata[ $base_id ][ $i ][ $id_key ] )
375 1
			: 0;
376
377
		return array(
378
			'value' => $this->text_url(),
379
			'supporting_field_value' => $id_val,
380
			'supporting_field_id'    => $id_key,
381
		);
382
	}
383
384
	/**
385
	 * Peforms saving of `file` attachement's ID
386
	 * @since  1.1.0
387
	 */
388
	public function _save_file_id_value( $file_id_key ) {
389
		$id_field = $this->_new_supporting_field( $file_id_key );
390
391
		// Check standard data_to_save data
392
		$id_val = isset( $this->field->data_to_save[ $file_id_key ] )
393
			? $this->field->data_to_save[ $file_id_key ]
394
			: null;
395
396
		// If there is no ID saved yet, try to get it from the url
397
		if ( $this->value && ! $id_val ) {
398
			$id_val = cmb2_utils()->image_id_from_url( $this->value );
399
		}
400
401
		return $id_field->save_field( $id_val );
402
	}
403
404
	/**
405
	 * Peforms saving of `text_datetime_timestamp_timezone` utc timestamp
406
	 * @since  2.2.0
407
	 */
408
	public function _save_utc_value( $utc_key, $utc_stamp ) {
409
		return $this->_new_supporting_field( $utc_key )->save_field( $utc_stamp );
410
	}
411
412
	/**
413
	 * Returns a new, supporting, CMB2_Field object based on a new field id.
414
	 * @since  2.2.0
415
	 */
416
	public function _new_supporting_field( $new_field_id ) {
417
		$args = $this->field->args();
418
		unset( $args['_id'], $args['_name'] );
419
420
		$args['id'] = $new_field_id;
421
		$args['sanitization_cb'] = false;
422
423
		// And get new field object
424
		return new CMB2_Field( array(
425
			'field_args'  => $args,
426
			'group_field' => $this->field->group,
427
			'object_id'   => $this->field->object_id,
428
			'object_type' => $this->field->object_type,
429
		) );
430
	}
431
432
	/**
433 1
	 * If repeating, loop through and re-apply sanitization method
434 1
	 * @since  1.1.0
435 1
	 * @param  string $method Class method
436
	 * @param  bool   $repeat Whether repeating or not
437
	 * @return mixed          Sanitized value
438 1
	 */
439
	public function _check_repeat( $method, $repeat ) {
440 1
		if ( $repeat || ! $this->field->args( 'repeatable' ) ) {
441 1
			return false;
442 1
		}
443 1
444 1
		$values_array = $this->value;
445 1
446 1
		$new_value = array();
447 1
		foreach ( $values_array as $iterator => $this->value ) {
448 1
			if ( $this->value ) {
449
				$val = $this->$method( true );
450 1
				if ( ! empty( $val ) ) {
451
					$new_value[] = $val;
452 1
				}
453
			}
454
		}
455
456
		$this->value = $new_value;
457
458
		return empty( $this->value ) ? null : $this->value;
459
	}
460
461 8
	/**
462 8
	 * Determine if passed value is an empty array
463 3
	 * @since  2.0.6
464 3
	 * @param  mixed  $to_check Value to check
465
	 * @return boolean          Whether value is an array that's empty
466 6
	 */
467
	public function _is_empty_array( $to_check ) {
468
		if ( is_array( $to_check ) ) {
469
			$cleaned_up = array_filter( $to_check );
470
			return empty( $cleaned_up );
471
		}
472
		return false;
473
	}
474
475
}
476