PluginProbe
AutomatorWP – No-Code Workflow Automation, Integration & Webhooks Plugin, now with AI / 6.0.3
AutomatorWP – No-Code Workflow Automation, Integration & Webhooks Plugin, now with AI v6.0.3
6.0.3 6.0.2 6.0.1 6.0.0 5.8.6 5.8.5 5.8.4 5.8.3 5.8.2 5.8.1 5.8.0 5.7.9.2 5.7.9.1 5.7.8 5.7.9 5.7.6 5.7.7 5.7.5 5.7.4 5.7.3 5.7.2 5.7.1 trunk 5.6.0 5.6.1 All 34 releases
← All changes | libraries/cmb2/includes/CMB2_Sanitize.php +665 -0 5.7.3 → 6.0.3 View file →
@@ -1,0 +1,665 @@
1 +<?php
2 +/**
3 + * CMB2 field sanitization
4 + *
5 + * @since 0.0.4
6 + *
7 + * @category WordPress_Plugin
8 + * @package CMB2
9 + * @author CMB2 team
10 + * @license GPL-2.0+
11 + * @link https://cmb2.io
12 + *
13 + * @method string _id()
14 + */
15 +class CMB2_Sanitize {
16 +
17 + /**
18 + * A CMB field object
19 + *
20 + * @var CMB2_Field object
21 + */
22 + public $field;
23 +
24 + /**
25 + * Field's value
26 + *
27 + * @var mixed
28 + */
29 + public $value;
30 +
31 + /**
32 + * Setup our class vars
33 + *
34 + * @since 1.1.0
35 + * @param CMB2_Field $field A CMB2 field object.
36 + * @param mixed $value Field value.
37 + */
38 + public function __construct( CMB2_Field $field, $value ) {
39 + $this->field = $field;
40 + $this->value = $value;
41 + }
42 +
43 + /**
44 + * Catchall method if field's 'sanitization_cb' is NOT defined,
45 + * or field type does not have a corresponding validation method.
46 + *
47 + * @since 1.0.0
48 + *
49 + * @param string $name Non-existent method name.
50 + * @param array $arguments All arguments passed to the method.
51 + * @return mixed
52 + */
53 + public function __call( $name, $arguments ) {
54 + return $this->default_sanitization();
55 + }
56 +
57 + /**
58 + * Default fallback sanitization method. Applies filters.
59 + *
60 + * @since 1.0.2
61 + */
62 + public function default_sanitization() {
63 + $field_type = $this->field->type();
64 +
65 + /**
66 + * This exists for back-compatibility, but validation
67 + * is not what happens here.
68 + *
69 + * @deprecated See documentation for "cmb2_sanitize_{$field_type}".
70 + */
71 + if ( function_exists( 'apply_filters_deprecated' ) ) {
72 + $override_value = apply_filters_deprecated( "cmb2_validate_{$field_type}", array( null, $this->value, $this->field->object_id, $this->field->args(), $this ), '2.0.0', "cmb2_sanitize_{$field_type}" );
73 + } else {
74 + $override_value = apply_filters( "cmb2_validate_{$field_type}", null, $this->value, $this->field->object_id, $this->field->args(), $this );
75 + }
76 +
77 + if ( null !== $override_value ) {
78 + return $override_value;
79 + }
80 +
81 + $sanitized_value = '';
82 + switch ( $field_type ) {
83 + case 'wysiwyg':
84 + case 'textarea_small':
85 + case 'oembed':
86 + $sanitized_value = $this->textarea();
87 + break;
88 + case 'taxonomy_select':
89 + case 'taxonomy_select_hierarchical':
90 + case 'taxonomy_radio':
91 + case 'taxonomy_radio_inline':
92 + case 'taxonomy_radio_hierarchical':
93 + case 'taxonomy_multicheck':
94 + case 'taxonomy_multicheck_hierarchical':
95 + case 'taxonomy_multicheck_inline':
96 + $sanitized_value = $this->taxonomy();
97 + break;
98 + case 'multicheck':
99 + case 'multicheck_inline':
100 + case 'file_list':
101 + case 'group':
102 + // no filtering
103 + $sanitized_value = $this->value;
104 + break;
105 + default:
106 + // Handle repeatable fields array
107 + // We'll fallback to 'sanitize_text_field'
108 + $sanitized_value = $this->_default_sanitization();
109 + break;
110 + }
111 +
112 + return $this->_is_empty_array( $sanitized_value ) ? '' : $sanitized_value;
113 + }
114 +
115 + /**
116 + * Default sanitization method, sanitize_text_field. Checks if value is array.
117 + *
118 + * @since 2.2.4
119 + * @return mixed Sanitized value.
120 + */
121 + protected function _default_sanitization() {
122 + // Handle repeatable fields array.
123 + return is_array( $this->value ) ? array_map( 'sanitize_text_field', $this->value ) : sanitize_text_field( $this->value );
124 + }
125 +
126 + /**
127 + * Sets the object terms to the object (if not options-page) and optionally returns the sanitized term values.
128 + *
129 + * @since 2.2.4
130 + * @return mixed Blank value, or sanitized term values if "cmb2_return_taxonomy_values_{$cmb_id}" is true.
131 + */
132 + public function taxonomy() {
133 + $sanitized_value = '';
134 +
135 + if ( ! $this->field->args( 'taxonomy' ) ) {
136 + CMB2_Utils::log_if_debug( __METHOD__, __LINE__, "{$this->field->type()} {$this->field->_id( '', false )} is missing the 'taxonomy' parameter." );
137 + } else {
138 +
139 + if ( in_array( $this->field->object_type, array( 'options-page', 'term' ), true ) ) {
140 + $return_values = true;
141 + } else {
142 + wp_set_object_terms( $this->field->object_id, $this->value, $this->field->args( 'taxonomy' ) );
143 + $return_values = false;
144 + }
145 +
146 + $cmb_id = $this->field->cmb_id;
147 +
148 + /**
149 + * Filter whether 'taxonomy_*' fields should return their value when being sanitized.
150 + *
151 + * By default, these fields do not return a value as we do not want them stored to meta
152 + * (as they are stored as terms). This allows overriding that and is used by CMB2::get_sanitized_values().
153 + *
154 + * The dynamic portion of the hook, $cmb_id, refers to the this field's CMB2 box id.
155 + *
156 + * @since 2.2.4
157 + *
158 + * @param bool $return_values By default, this is only true for 'options-page' boxes. To enable:
159 + * `add_filter( "cmb2_return_taxonomy_values_{$cmb_id}", '__return_true' );`
160 + * @param CMB2_Sanitize $sanitizer This object.
161 + */
162 + if ( apply_filters( "cmb2_return_taxonomy_values_{$cmb_id}", $return_values, $this ) ) {
163 + $sanitized_value = $this->_default_sanitization();
164 + }
165 + }
166 +
167 + return $sanitized_value;
168 + }
169 +
170 + /**
171 + * Simple checkbox validation
172 + *
173 + * @since 1.0.1
174 + * @return string|false 'on' or false
175 + */
176 + public function checkbox() {
177 + return $this->value === 'on' ? 'on' : false;
178 + }
179 +
180 + /**
181 + * Validate url in a meta value.
182 + *
183 + * @since 1.0.1
184 + * @return string Empty string or escaped url
185 + */
186 + public function text_url() {
187 + $protocols = $this->field->args( 'protocols' );
188 + $default = $this->field->get_default();
189 +
190 + // for repeatable.
191 + if ( is_array( $this->value ) ) {
192 + foreach ( $this->value as $key => $val ) {
193 + $this->value[ $key ] = self::sanitize_and_secure_url( $val, $protocols, $default );
194 + }
195 + } else {
196 + $this->value = self::sanitize_and_secure_url( $this->value, $protocols, $default );
197 + }
198 +
199 + return $this->value;
200 + }
201 +
202 + public function colorpicker() {
203 + // for repeatable.
204 + if ( is_array( $this->value ) ) {
205 + $check = $this->value;
206 + $this->value = array();
207 + foreach ( $check as $key => $val ) {
208 + if ( $val && '#' != $val ) {
209 + $this->value[ $key ] = esc_attr( $val );
210 + }
211 + }
212 + } else {
213 + $this->value = ! $this->value || '#' == $this->value ? '' : esc_attr( $this->value );
214 + }
215 + return $this->value;
216 + }
217 +
218 + /**
219 + * Validate email in a meta value
220 + *
221 + * @since 1.0.1
222 + * @return string Empty string or sanitized email
223 + */
224 + public function text_email() {
225 + // for repeatable.
226 + if ( is_array( $this->value ) ) {
227 + foreach ( $this->value as $key => $val ) {
228 + $val = trim( $val );
229 + $this->value[ $key ] = is_email( $val ) ? $val : '';
230 + }
231 + } else {
232 + $this->value = trim( $this->value );
233 + $this->value = is_email( $this->value ) ? $this->value : '';
234 + }
235 +
236 + return $this->value;
237 + }
238 +
239 + /**
240 + * Validate money in a meta value
241 + *
242 + * @since 1.0.1
243 + * @return string Empty string or sanitized money value
244 + */
245 + public function text_money() {
246 + if ( ! $this->value ) {
247 + return '';
248 + }
249 +
250 + global $wp_locale;
251 +
252 + $search = array( $wp_locale->number_format['thousands_sep'], $wp_locale->number_format['decimal_point'] );
253 + $replace = array( '', '.' );
254 +
255 + // Strip slashes. Example: 2\'180.00.
256 + // See https://github.com/CMB2/CMB2/issues/1014.
257 + $this->value = wp_unslash( $this->value );
258 +
259 + // for repeatable.
260 + if ( is_array( $this->value ) ) {
261 + foreach ( $this->value as $key => $val ) {
262 + if ( $val ) {
263 + $this->value[ $key ] = number_format_i18n( (float) str_ireplace( $search, $replace, $val ), 2 );
264 + }
265 + }
266 + } else {
267 + $this->value = number_format_i18n( (float) str_ireplace( $search, $replace, $this->value ), 2 );
268 + }
269 +
270 + return $this->value;
271 + }
272 +
273 + /**
274 + * Converts text date to timestamp
275 + *
276 + * @since 1.0.2
277 + * @return string Timestring
278 + */
279 + public function text_date_timestamp() {
280 + // date_create_from_format if there is a slash in the value.
281 + $this->value = wp_unslash( $this->value );
282 +
283 + return is_array( $this->value )
284 + ? array_map( array( $this->field, 'get_timestamp_from_value' ), $this->value )
285 + : $this->field->get_timestamp_from_value( $this->value );
286 + }
287 +
288 + /**
289 + * Datetime to timestamp
290 + *
291 + * @since 1.0.1
292 + *
293 + * @param bool $repeat Whether or not to repeat.
294 + * @return string|array Timestring
295 + */
296 + public function text_datetime_timestamp( $repeat = false ) {
297 + // date_create_from_format if there is a slash in the value.
298 + $this->value = wp_unslash( $this->value );
299 +
300 + if ( $this->is_empty_value() ) {
301 + return '';
302 + }
303 +
304 + $repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
305 + if ( false !== $repeat_value ) {
306 + return $repeat_value;
307 + }
308 +
309 + // Account for timestamp values passed through REST API.
310 + if ( $this->is_valid_date_value() ) {
311 +
312 + $this->value = CMB2_Utils::make_valid_time_stamp( $this->value );
313 +
314 + } elseif ( isset( $this->value['date'], $this->value['time'] ) ) {
315 + $this->value = $this->field->get_timestamp_from_value( $this->value['date'] . ' ' . $this->value['time'] );
316 + }
317 +
318 + if ( $tz_offset = $this->field->field_timezone_offset() ) {
319 + $this->value += (int) $tz_offset;
320 + }
321 +
322 + return $this->value;
323 + }
324 +
325 + /**
326 + * Datetime to timestamp with timezone
327 + *
328 + * @since 1.0.1
329 + *
330 + * @param bool $repeat Whether or not to repeat.
331 + * @return string Timestring
332 + */
333 + public function text_datetime_timestamp_timezone( $repeat = false ) {
334 + static $utc_values = array();
335 +
336 + if ( $this->is_empty_value() ) {
337 + return '';
338 + }
339 +
340 + // date_create_from_format if there is a slash in the value.
341 + $this->value = wp_unslash( $this->value );
342 +
343 + $utc_key = $this->field->_id( '', false ) . '_utc';
344 +
345 + $repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
346 + if ( false !== $repeat_value ) {
347 + if ( ! empty( $utc_values[ $utc_key ] ) ) {
348 + $this->_save_utc_value( $utc_key, $utc_values[ $utc_key ] );
349 + unset( $utc_values[ $utc_key ] );
350 + }
351 +
352 + return $repeat_value;
353 + }
354 +
355 + $tzstring = null;
356 +
357 + if ( is_array( $this->value ) && array_key_exists( 'timezone', $this->value ) ) {
358 + $tzstring = $this->value['timezone'];
359 + }
360 +
361 + if ( empty( $tzstring ) ) {
362 + $tzstring = CMB2_Utils::timezone_string();
363 + }
364 +
365 + $offset = CMB2_Utils::timezone_offset( $tzstring );
366 +
367 + if ( 'UTC' === substr( $tzstring, 0, 3 ) ) {
368 + $tzstring = timezone_name_from_abbr( '', $offset, 0 );
369 + /**
370 + * The timezone_name_from_abbr() returns false if not found based on offset.
371 + * Since there are currently some invalid timezones in wp_timezone_dropdown(),
372 + * fallback to an offset of 0 (UTC+0)
373 + * https://core.trac.wordpress.org/ticket/29205
374 + */
375 + $tzstring = false !== $tzstring ? $tzstring : timezone_name_from_abbr( '', 0, 0 );
376 + }
377 +
378 + $full_format = $this->field->args['date_format'] . ' ' . $this->field->args['time_format'];
379 +
380 + try {
381 + $datetime = null;
382 +
383 + if ( is_array( $this->value ) ) {
384 +
385 + $full_date = $this->value['date'] . ' ' . $this->value['time'];
386 + $datetime = date_create_from_format( $full_format, $full_date );
387 +
388 + } elseif ( $this->is_valid_date_value() ) {
389 +
390 + $timestamp = CMB2_Utils::make_valid_time_stamp( $this->value );
391 + if ( $timestamp ) {
392 + $datetime = new DateTime();
393 + $datetime->setTimestamp( $timestamp );
394 + }
395 + }
396 +
397 + if ( ! is_object( $datetime ) ) {
398 + $this->value = $utc_stamp = '';
399 + } else {
400 + $datetime->setTimezone( new DateTimeZone( $tzstring ) );
401 + $utc_stamp = date_timestamp_get( $datetime ) - $offset;
402 + $this->value = json_encode( $datetime );
403 + }
404 +
405 + if ( $this->field->group ) {
406 + $this->value = array(
407 + 'supporting_field_value' => $utc_stamp,
408 + 'supporting_field_id' => $utc_key,
409 + 'value' => $this->value,
410 + );
411 + } else {
412 + // Save the utc timestamp supporting field.
413 + if ( $repeat ) {
414 + $utc_values[ $utc_key ][] = $utc_stamp;
415 + } else {
416 + $this->_save_utc_value( $utc_key, $utc_stamp );
417 + }
418 + }
419 + } catch ( Exception $e ) {
420 + $this->value = '';
421 + CMB2_Utils::log_if_debug( __METHOD__, __LINE__, $e->getMessage() );
422 + }
423 +
424 + return $this->value;
425 + }
426 +
427 + /**
428 + * Sanitize textareas and wysiwyg fields
429 + *
430 + * @since 1.0.1
431 + * @return string Sanitized data
432 + */
433 + public function textarea() {
434 + return is_array( $this->value ) ? array_map( 'wp_kses_post', $this->value ) : ( $this->value !== null ? wp_kses_post( $this->value ) : '' );
435 + }
436 +
437 + /**
438 + * Sanitize code textareas
439 + *
440 + * @since 1.0.2
441 + *
442 + * @param bool $repeat Whether or not to repeat.
443 + * @return string Sanitized data
444 + */
445 + public function textarea_code( $repeat = false ) {
446 + $repeat_value = $this->_check_repeat( __FUNCTION__, $repeat );
447 + if ( false !== $repeat_value ) {
448 + return $repeat_value;
449 + }
450 +
451 + return htmlspecialchars_decode( stripslashes( $this->value ), ENT_COMPAT );
452 + }
453 +
454 + /**
455 + * Handles saving of attachment post ID and sanitizing file url
456 + *
457 + * @since 1.1.0
458 + * @return string Sanitized url
459 + */
460 + public function file() {
461 + $file_id_key = $this->field->_id( '', false ) . '_id';
462 +
463 + if ( $this->field->group ) {
464 + // Return an array with url/id if saving a group field.
465 + $this->value = $this->_get_group_file_value_array( $file_id_key );
466 + } else {
467 + $this->_save_file_id_value( $file_id_key );
468 + $this->text_url();
469 + }
470 +
471 + return $this->value;
472 + }
473 +
474 + /**
475 + * Gets the values for the `file` field type from the data being saved.
476 + *
477 + * @since 2.2.0
478 + *
479 + * @param mixed $id_key ID key to use.
480 + * @return array
481 + */
482 + public function _get_group_file_value_array( $id_key ) {
483 + $alldata = $this->field->group->data_to_save;
484 + $base_id = $this->field->group->_id( '', false );
485 + $i = $this->field->group->index;
486 +
487 + // Check group $alldata data.
488 + $id_val = isset( $alldata[ $base_id ][ $i ][ $id_key ] )
489 + ? absint( $alldata[ $base_id ][ $i ][ $id_key ] )
490 + : '';
491 +
492 + // We don't want to save 0 to the DB for file fields.
493 + if ( 0 === $id_val ) {
494 + $id_val = '';
495 + }
496 +
497 + return array(
498 + 'value' => $this->text_url(),
499 + 'supporting_field_value' => $id_val,
500 + 'supporting_field_id' => $id_key,
501 + );
502 + }
503 +
504 + /**
505 + * Peforms saving of `file` attachement's ID
506 + *
507 + * @since 1.1.0
508 + *
509 + * @param mixed $file_id_key ID key to use.
510 + * @return mixed
511 + */
512 + public function _save_file_id_value( $file_id_key ) {
513 + $id_field = $this->_new_supporting_field( $file_id_key );
514 +
515 + // Check standard data_to_save data.
516 + $id_val = isset( $this->field->data_to_save[ $file_id_key ] )
517 + ? $this->field->data_to_save[ $file_id_key ]
518 + : null;
519 +
520 + // If there is no ID saved yet, try to get it from the url.
521 + if ( $this->value && ! $id_val ) {
522 + $id_val = CMB2_Utils::image_id_from_url( $this->value );
523 +
524 + // If there is an ID but user emptied the input value, remove the ID.
525 + } elseif ( ! $this->value && $id_val ) {
526 + $id_val = null;
527 + }
528 +
529 + return $id_field->save_field( $id_val );
530 + }
531 +
532 + /**
533 + * Peforms saving of `text_datetime_timestamp_timezone` utc timestamp
534 + *
535 + * @since 2.2.0
536 + *
537 + * @param mixed $utc_key UTC key.
538 + * @param mixed $utc_stamp UTC timestamp.
539 + * @return mixed
540 + */
541 + public function _save_utc_value( $utc_key, $utc_stamp ) {
542 + return $this->_new_supporting_field( $utc_key )->save_field( $utc_stamp );
543 + }
544 +
545 + /**
546 + * Returns a new, supporting, CMB2_Field object based on a new field id.
547 + *
548 + * @since 2.2.0
549 + *
550 + * @param mixed $new_field_id New field ID.
551 + * @return CMB2_Field
552 + */
553 + public function _new_supporting_field( $new_field_id ) {
554 + return $this->field->get_field_clone( array(
555 + 'id' => $new_field_id,
556 + 'sanitization_cb' => false,
557 + ) );
558 + }
559 +
560 + /**
561 + * If repeating, loop through and re-apply sanitization method
562 + *
563 + * @since 1.1.0
564 + * @param string $method Class method.
565 + * @param bool $repeat Whether repeating or not.
566 + * @return mixed Sanitized value
567 + */
568 + public function _check_repeat( $method, $repeat ) {
569 + if ( $repeat || ! $this->field->args( 'repeatable' ) ) {
570 + return false;
571 + }
572 +
573 + $values_array = $this->value;
574 +
575 + $new_value = array();
576 + foreach ( $values_array as $this->value ) {
577 + if ( $this->value ) {
578 + $val = $this->$method( true );
579 + if ( ! empty( $val ) ) {
580 + $new_value[] = $val;
581 + }
582 + }
583 + }
584 +
585 + $this->value = $new_value;
586 +
587 + return empty( $this->value ) ? null : $this->value;
588 + }
589 +
590 + /**
591 + * Determine if passed value is an empty array
592 + *
593 + * @since 2.0.6
594 + * @param mixed $to_check Value to check.
595 + * @return boolean Whether value is an array that's empty
596 + */
597 + public function _is_empty_array( $to_check ) {
598 + if ( is_array( $to_check ) ) {
599 + $cleaned_up = array_filter( $to_check );
600 + return empty( $cleaned_up );
601 + }
602 + return false;
603 + }
604 +
605 + /**
606 + * Sanitize a URL. Make the default scheme HTTPS.
607 + *
608 + * @since 2.10.0
609 + * @param string $value Unescaped URL.
610 + * @param array $protocols Allowed protocols for URL.
611 + * @param string $default Default value if no URL found.
612 + * @return string escaped URL.
613 + */
614 + public static function sanitize_and_secure_url( $url, $protocols = null, $default = null ) {
615 + if ( empty( $url ) ) {
616 + return $default;
617 + }
618 +
619 + $orig_scheme = parse_url( $url, PHP_URL_SCHEME );
620 + $url = esc_url_raw( $url, $protocols );
621 +
622 + // If original url has no scheme...
623 + if ( null === $orig_scheme ) {
624 +
625 + // Let's make sure the added scheme is https.
626 + $url = set_url_scheme( $url, 'https' );
627 + }
628 +
629 + return $url;
630 + }
631 +
632 + /**
633 + * Check if the current field's value is empty.
634 + *
635 + * @since 2.9.1
636 + *
637 + * @return boolean Wether value is empty.
638 + */
639 + public function is_empty_value() {
640 + if ( empty( $this->value ) ) {
641 + return true;
642 + }
643 +
644 + if ( is_array( $this->value ) ) {
645 + $test = array_filter( $this->value );
646 + if ( empty( $test ) ) {
647 + return true;
648 + }
649 + }
650 +
651 + return false;
652 + }
653 +
654 + /**
655 + * Check if the current field's value is a valid date value.
656 + *
657 + * @since 2.9.1
658 + *
659 + * @return boolean Wether value is a valid date value.
660 + */
661 + public function is_valid_date_value() {
662 + return is_scalar( $this->value ) && CMB2_Utils::is_valid_date( $this->value );
663 + }
664 +
665 +}