PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.2
WebberZone Top 10 — Popular Posts v4.5.2
4.5.2 4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 All 118 releases
top-10 / includes / admin / settings / class-settings-sanitize.php

class-settings-sanitize.php in WebberZone Top 10 — Popular Posts 4.5.2, at includes/admin/settings/class-settings-sanitize.php

761 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Functions to sanitize settings.
4 *
5 * @link https://webberzone.com
6 *
7 * @package WebberZone\Top_Ten
8 */
9
10 namespace WebberZone\Top_Ten\Admin\Settings;
11
12 // If this file is called directly, abort.
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 /**
18 * Settings Sanitize Class.
19 *
20 * @since 4.0.0
21 */
22 class Settings_Sanitize {
23
24 /**
25 * Settings Key.
26 *
27 * @var string Settings Key.
28 */
29 public $settings_key;
30
31 /**
32 * Prefix which is used for creating the unique filters and actions.
33 *
34 * @var string Prefix.
35 */
36 public $prefix;
37
38 /**
39 * Main constructor class.
40 *
41 * @param mixed $args {
42 * Array or string of arguments. Default is blank array.
43 * @type string $settings_key Settings key.
44 * @type string $prefix Prefix.
45 * }
46 */
47 public function __construct( $args ) {
48 $defaults = array(
49 'settings_key' => '',
50 'prefix' => '',
51 );
52 $args = wp_parse_args( $args, $defaults );
53
54 foreach ( $args as $name => $value ) {
55 $this->$name = $value;
56 }
57 }
58
59 /**
60 * Get the value of a settings field.
61 *
62 * @param string $option Settings field name.
63 * @param mixed $default_value Default value if option is not found.
64 * @return mixed
65 */
66 public function get_option( $option, $default_value = '' ) {
67 $options = \get_option( $this->settings_key );
68
69 if ( isset( $options[ $option ] ) ) {
70 return $options[ $option ];
71 }
72
73 return $default_value;
74 }
75
76 /**
77 * Fallback for field types that declare no sanitize callback of their own.
78 *
79 * @param mixed $value Setting Value.
80 * @return mixed Sanitized value.
81 */
82 public function sanitize_missing( $value ) {
83 if ( is_array( $value ) ) {
84 $sanitized = array();
85
86 foreach ( $value as $key => $item ) {
87 $sanitized[ sanitize_text_field( (string) $key ) ] = $this->sanitize_missing( $item );
88 }
89
90 return $sanitized;
91 }
92
93 if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
94 return $value;
95 }
96
97 if ( is_object( $value ) || is_null( $value ) ) {
98 return '';
99 }
100
101 return sanitize_text_field( wp_unslash( (string) $value ) );
102 }
103
104 /**
105 * Sanitize text fields
106 *
107 * @param string $value The field value.
108 * @return string Sanitizied value
109 */
110 public function sanitize_text_field( $value ) {
111 return $this->sanitize_textarea_field( $value );
112 }
113
114 /**
115 * Sanitize site-local date/time fields.
116 *
117 * @param string $value The date/time value.
118 * @return string Sanitized date/time, or an empty string when invalid.
119 */
120 public function sanitize_datetime_field( $value ) {
121 $value = trim( sanitize_text_field( wp_unslash( (string) $value ) ), " \t\n\r\0\x0B" );
122 $format = '!Y-m-d\\TH:i';
123 $utc = \DateTimeImmutable::createFromFormat( $format, $value, new \DateTimeZone( 'UTC' ) );
124 $errors = \DateTimeImmutable::getLastErrors();
125
126 if ( false === $utc || ( false !== $errors && ( $errors['warning_count'] > 0 || $errors['error_count'] > 0 ) ) || $utc->format( 'Y-m-d\\TH:i' ) !== $value ) {
127 return '';
128 }
129
130 $local = \DateTimeImmutable::createFromFormat( $format, $value, wp_timezone() );
131 if ( false === $local || $local->format( 'Y-m-d\\TH:i' ) !== $value ) {
132 return '';
133 }
134
135 return $value;
136 }
137
138 /**
139 * Sanitize number fields
140 *
141 * @param string $value The field value.
142 * @return string Sanitized value
143 */
144 public function sanitize_number_field( $value ) {
145 return filter_var( $value, FILTER_SANITIZE_NUMBER_INT );
146 }
147
148 /**
149 * Sanitize CSV fields
150 *
151 * @param string $value The field value.
152 * @return string Sanitizied value
153 */
154 public function sanitize_csv_field( $value ) {
155 return implode( ',', array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) );
156 }
157
158 /**
159 * Sanitize CSV fields which hold numbers
160 *
161 * @param string $value The field value.
162 * @return string Sanitized value
163 */
164 public function sanitize_numbercsv_field( $value ) {
165 return implode( ',', array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ) );
166 }
167
168 /**
169 * Sanitize CSV fields which hold post IDs
170 *
171 * @param string $value The field value.
172 * @return string Sanitized value
173 */
174 public function sanitize_postids_field( $value ) {
175 $ids = array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) );
176
177 foreach ( $ids as $key => $value ) {
178 if ( false === get_post_status( $value ) ) {
179 unset( $ids[ $key ] );
180 }
181 }
182
183 return implode( ',', $ids );
184 }
185
186 /**
187 * Sanitize textarea fields
188 *
189 * @param string $value The field value.
190 * @return string Sanitized value
191 */
192 public function sanitize_textarea_field( $value ) {
193
194 if ( ! current_user_can( 'unfiltered_html' ) ) {
195 return wp_kses_post( wp_unslash( $value ) );
196 }
197
198 global $allowedposttags;
199
200 // We need more tags to allow for script and style.
201 $moretags = array(
202 'script' => array(
203 'type' => true,
204 'src' => true,
205 'async' => true,
206 'defer' => true,
207 'charset' => true,
208 ),
209 'style' => array(
210 'type' => true,
211 'media' => true,
212 'scoped' => true,
213 ),
214 'link' => array(
215 'rel' => true,
216 'type' => true,
217 'href' => true,
218 'media' => true,
219 'sizes' => true,
220 'hreflang' => true,
221 ),
222 );
223
224 $allowedtags = array_merge( $allowedposttags, $moretags );
225
226 /**
227 * Filter allowed tags allowed when sanitizing text and textarea fields.
228 *
229 * @param array $allowedtags Allowed tags array.
230 */
231 $allowedtags = apply_filters( $this->prefix . '_sanitize_allowed_tags', $allowedtags ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
232
233 return wp_kses( wp_unslash( $value ), $allowedtags );
234 }
235
236 /**
237 * Sanitize checkbox fields
238 *
239 * @param mixed $value The field value.
240 * @return int Sanitized value
241 */
242 public function sanitize_checkbox_field( $value ) {
243 $value = in_array( (int) $value, array( 0, -1 ), true ) ? 0 : 1;
244
245 return $value;
246 }
247
248 /**
249 * Sanitize toggle fields
250 *
251 * @param mixed $value The field value.
252 * @return int Sanitized value
253 */
254 public function sanitize_toggle_field( $value ) {
255 return $this->sanitize_checkbox_field( $value );
256 }
257
258 /**
259 * Sanitize multicheck fields
260 *
261 * @param array|int $value The field value.
262 * @return string $value Sanitized value
263 */
264 public function sanitize_multicheck_field( $value ) {
265 $values = ( -1 === (int) $value ) ? array() : array_map( 'sanitize_text_field', (array) wp_unslash( $value ) );
266
267 return implode( ',', $values );
268 }
269
270 /**
271 * Sanitize post_types fields
272 *
273 * @param array|int $value The field value.
274 * @return string $value Sanitized value
275 */
276 public function sanitize_posttypes_field( $value ) {
277 return $this->sanitize_multicheck_field( $value );
278 }
279
280 /**
281 * Sanitize post_types fields
282 *
283 * @param array|int $value The field value.
284 * @return string $value Sanitized value
285 */
286 public function sanitize_taxonomies_field( $value ) {
287 return $this->sanitize_multicheck_field( $value );
288 }
289
290 /**
291 * Sanitize color fields.
292 *
293 * @param string $value The field value.
294 * @return string Sanitized value
295 */
296 public function sanitize_color_field( $value ) {
297 return sanitize_hex_color( $value );
298 }
299
300 /**
301 * Sanitize email fields.
302 *
303 * @param string $value The field value.
304 * @return string Sanitized value
305 */
306 public function sanitize_email_field( $value ) {
307 return sanitize_email( $value );
308 }
309
310 /**
311 * Sanitize URL fields.
312 *
313 * @param string $value The field value.
314 * @return string Sanitized value
315 */
316 public function sanitize_url_field( $value ) {
317 return esc_url_raw( $value );
318 }
319
320 /**
321 * Sanitize file fields, which hold the URL picked in the media browser.
322 *
323 * @param string $value The field value.
324 * @return string Sanitized value
325 */
326 public function sanitize_file_field( $value ) {
327 return esc_url_raw( wp_unslash( $value ) );
328 }
329
330 /**
331 * Sanitize password fields.
332 *
333 * @param string $value The field value.
334 * @return string Sanitized value
335 */
336 public function sanitize_password_field( $value ) {
337 return sanitize_text_field( wp_unslash( $value ) );
338 }
339
340 /**
341 * Sanitize WYSIWYG fields.
342 *
343 * @param string $value The field value.
344 * @return string Sanitized value
345 */
346 public function sanitize_wysiwyg_field( $value ) {
347 return wp_kses_post( wp_unslash( $value ) );
348 }
349
350 /**
351 * Sanitize HTML fields.
352 *
353 * @param string $value The field value.
354 * @return string Sanitized value
355 */
356 public function sanitize_html_field( $value ) {
357 return $this->sanitize_textarea_field( $value );
358 }
359
360 /**
361 * Sanitize CSS fields.
362 *
363 * @param string $value The field value.
364 * @return string Sanitized value
365 */
366 public function sanitize_css_field( $value ) {
367 return wp_strip_all_tags( wp_unslash( $value ) );
368 }
369
370 /**
371 * Sanitize radio fields against the options the field actually offers.
372 *
373 * @param mixed $value The field value.
374 * @param array $field Field configuration array.
375 * @return string Sanitized value
376 */
377 public function sanitize_radio_field( $value, $field = array() ) {
378 return $this->sanitize_choice( $value, array_keys( (array) ( $field['options'] ?? array() ) ), $field );
379 }
380
381 /**
382 * Sanitize select fields against the options the field actually offers.
383 *
384 * @param mixed $value The field value.
385 * @param array $field Field configuration array.
386 * @return string Sanitized value
387 */
388 public function sanitize_select_field( $value, $field = array() ) {
389 return $this->sanitize_choice( $value, array_keys( (array) ( $field['options'] ?? array() ) ), $field );
390 }
391
392 /**
393 * Sanitize radio fields that carry a description per option.
394 *
395 * @param mixed $value The field value.
396 * @param array $field Field configuration array.
397 * @return string Sanitized value
398 */
399 public function sanitize_radiodesc_field( $value, $field = array() ) {
400 $allowed = array();
401
402 foreach ( (array) ( $field['options'] ?? array() ) as $option ) {
403 if ( isset( $option['id'] ) ) {
404 $allowed[] = $option['id'];
405 }
406 }
407
408 return $this->sanitize_choice( $value, $allowed, $field );
409 }
410
411 /**
412 * Sanitize thumbnail size fields.
413 *
414 * @param mixed $value The field value.
415 * @param array $field Field configuration array.
416 * @return string Sanitized value
417 */
418 public function sanitize_thumbsizes_field( $value, $field = array() ) {
419 $allowed = array_keys( (array) ( $field['options'] ?? array() ) );
420
421 // The form injects this size at render time, so it is never in the registered options.
422 $allowed[] = $this->prefix . '_thumbnail';
423
424 return $this->sanitize_choice( $value, $allowed, $field );
425 }
426
427 /**
428 * Restrict a value to a list of allowed choices.
429 *
430 * @param mixed $value The field value.
431 * @param array $allowed Allowed choices.
432 * @param array $field Field configuration array.
433 * @return string Sanitized value
434 */
435 protected function sanitize_choice( $value, $allowed, $field = array() ) {
436 $value = sanitize_text_field( wp_unslash( (string) $value ) );
437 $allowed = array_map( 'strval', (array) $allowed );
438
439 if ( in_array( $value, $allowed, true ) ) {
440 return $value;
441 }
442
443 // The select callback prints option values through sanitize_key().
444 foreach ( $allowed as $choice ) {
445 if ( sanitize_key( $choice ) === $value ) {
446 return $choice;
447 }
448 }
449
450 if ( isset( $field['default'] ) ) {
451 return (string) $field['default'];
452 }
453
454 return empty( $allowed ) ? '' : reset( $allowed );
455 }
456
457 /**
458 * Sanitize sensitive fields.
459 *
460 * @param string $value The field value.
461 * @param string|array $key The field key.
462 * @return string Sanitized value
463 */
464 public function sanitize_sensitive_field( $value, $key ) {
465 if ( is_array( $key ) ) {
466 if ( isset( $key['id'] ) ) {
467 $key = $key['id'];
468 } else {
469 return $value;
470 }
471 }
472
473 $stored_encrypted_key = $this->get_option( $key );
474
475 // Empty input clears the stored value.
476 if ( '' === (string) $value ) {
477 return '';
478 }
479
480 // If input is masked, return existing encrypted key.
481 if ( strpos( (string) $value, '**' ) !== false ) {
482 return $stored_encrypted_key;
483 }
484
485 return Settings_API::encrypt_api_key( $value );
486 }
487
488 /**
489 * Sanitize repeater field.
490 *
491 * @param mixed $value Array of repeater values (may be non-array from form data).
492 * @param array $field Field configuration array.
493 * @return array Sanitized array
494 */
495 public function sanitize_repeater_field( $value, $field = array() ) {
496 // No usable controls are rendered, so submitted rows are forged.
497 if ( ! empty( $field['disabled'] ) || ! empty( $field['pro'] ) ) {
498 $stored = ! empty( $field['id'] ) ? $this->get_option( $field['id'], array() ) : array();
499 return is_array( $stored ) ? $stored : array();
500 }
501
502 if ( ! is_array( $value ) ) {
503 return array();
504 }
505
506 $sanitized_value = array();
507 $existing_rows = array();
508
509 // Get the subfields configuration.
510 $subfields = ! empty( $field['fields'] ) ? $field['fields'] : array();
511 if ( ! empty( $field['id'] ) ) {
512 $stored_value = $this->get_option( $field['id'], array() );
513 $existing_rows = is_array( $stored_value ) ? $stored_value : array();
514 }
515
516 // Create a lookup table for existing rows by row_id.
517 $existing_by_id = array();
518 foreach ( $existing_rows as $existing_row ) {
519 if ( isset( $existing_row['row_id'] ) ) {
520 $existing_by_id[ $existing_row['row_id'] ] = $existing_row;
521 }
522 }
523
524 foreach ( $value as $index => $row ) {
525 // Ensure we have a valid row structure.
526 if ( ! isset( $row['fields'] ) || ! is_array( $row['fields'] ) ) {
527 continue;
528 }
529
530 $sanitized_row = array(
531 'fields' => array(),
532 );
533
534 // Preserve row_id if it exists.
535 if ( isset( $row['row_id'] ) ) {
536 $sanitized_row['row_id'] = sanitize_text_field( $row['row_id'] );
537 }
538
539 // Get the corresponding existing row for sensitive field preservation.
540 $existing_row = null;
541 if ( isset( $row['row_id'] ) && isset( $existing_by_id[ $row['row_id'] ] ) ) {
542 $existing_row = $existing_by_id[ $row['row_id'] ];
543 }
544
545 foreach ( $row['fields'] as $field_key => $field_value ) {
546 $field_key = sanitize_key( $field_key );
547
548 // Skip if field_key is not in our subfields configuration.
549 $field_config = null;
550 foreach ( $subfields as $subfield ) {
551 if ( isset( $subfield['id'] ) && $subfield['id'] === $field_key ) {
552 $field_config = $subfield;
553 break;
554 }
555 }
556
557 if ( null === $field_config ) {
558 continue;
559 }
560
561 // Get the field type from the subfield configuration.
562 $field_type = isset( $field_config['type'] ) ? $field_config['type'] : 'text';
563
564 // For sensitive fields, distinguish empty (clear) from masked (preserve).
565 if ( 'sensitive' === $field_type ) {
566 if ( '' === (string) $field_value ) {
567 $sanitized_row['fields'][ $field_key ] = '';
568 continue;
569 }
570 if ( is_string( $field_value ) && false !== strpos( $field_value, '**' ) ) {
571 if ( $existing_row && isset( $existing_row['fields'][ $field_key ] ) ) {
572 $sanitized_row['fields'][ $field_key ] = $existing_row['fields'][ $field_key ];
573 }
574 continue;
575 }
576 }
577
578 // Call the appropriate sanitization method.
579 $sanitize_method = 'sanitize_' . $field_type . '_field';
580 if ( method_exists( $this, $sanitize_method ) ) {
581 if ( 'sensitive' === $field_type ) {
582 $sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_key );
583 } else {
584 $sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
585 }
586 } else {
587 $sanitized_row['fields'][ $field_key ] = $this->sanitize_text_field( $field_value );
588 }
589 }
590
591 if ( ! empty( $sanitized_row['fields'] ) ) {
592 $sanitized_value[ $index ] = $sanitized_row;
593 }
594 }
595
596 return $sanitized_value;
597 }
598
599 /**
600 * Find repeater rows that fail their own required-field rules.
601 *
602 * Purely structural - returns what is wrong, not a human message, so it carries no i18n.
603 *
604 * @param array $rows Sanitized repeater rows, as returned by sanitize_repeater_field().
605 * @param array $field Repeater field configuration.
606 * @return array Map of row index => issue, where issue may have a 'missing' key
607 * (subfield IDs with required => true that are empty) and/or a
608 * 'missing_one_of' key (the required_one_of group, present only when
609 * none of it is filled).
610 */
611 public static function get_incomplete_repeater_rows( array $rows, array $field ) {
612 $subfields = ! empty( $field['fields'] ) && is_array( $field['fields'] ) ? $field['fields'] : array();
613
614 $required_subfields = array();
615 foreach ( $subfields as $subfield_id => $subfield ) {
616 // Row values are keyed by the subfield's own id; the array key may be numeric.
617 if ( ! empty( $subfield['required'] ) ) {
618 $required_subfields[] = $subfield['id'] ?? $subfield_id;
619 }
620 }
621
622 $required_one_of = ! empty( $field['required_one_of'] ) && is_array( $field['required_one_of'] ) ? $field['required_one_of'] : array();
623
624 if ( empty( $required_subfields ) && empty( $required_one_of ) ) {
625 return array();
626 }
627
628 $is_filled = static function ( $values, $subfield_id ) {
629 return '' !== trim( (string) ( $values[ $subfield_id ] ?? '' ), " \t\n\r\0\x0B," );
630 };
631 $incomplete = array();
632
633 foreach ( array_values( $rows ) as $index => $row ) {
634 $values = isset( $row['fields'] ) && is_array( $row['fields'] ) ? $row['fields'] : array();
635 $issue = array();
636
637 $missing = array();
638 foreach ( $required_subfields as $subfield_id ) {
639 if ( ! $is_filled( $values, $subfield_id ) ) {
640 $missing[] = $subfield_id;
641 }
642 }
643 if ( ! empty( $missing ) ) {
644 $issue['missing'] = $missing;
645 }
646
647 if ( ! empty( $required_one_of ) ) {
648 $filled = false;
649 foreach ( $required_one_of as $subfield_id ) {
650 if ( $is_filled( $values, $subfield_id ) ) {
651 $filled = true;
652 break;
653 }
654 }
655 if ( ! $filled ) {
656 $issue['missing_one_of'] = $required_one_of;
657 }
658 }
659
660 if ( ! empty( $issue ) ) {
661 $incomplete[ $index ] = $issue;
662 }
663 }
664
665 return $incomplete;
666 }
667
668 /**
669 * Convert a string to CSV.
670 *
671 * @param array $input_array Input string.
672 * @param string $delimiter Delimiter.
673 * @param string $enclosure Enclosure.
674 * @param string $terminator Terminating string.
675 * @return string CSV string.
676 */
677 public static function str_putcsv( $input_array, $delimiter = ',', $enclosure = '"', $terminator = "\n" ) {
678 // First convert associative array to numeric indexed array.
679 $work_array = array();
680 foreach ( $input_array as $key => $value ) {
681 $work_array[] = $value;
682 }
683
684 $output = '';
685 $array_size = count( $work_array );
686
687 for ( $i = 0; $i < $array_size; $i++ ) {
688 // Nested array, process nest item.
689 if ( is_array( $work_array[ $i ] ) ) {
690 $output .= self::str_putcsv( $work_array[ $i ], $delimiter, $enclosure, $terminator );
691 } else {
692 switch ( gettype( $work_array[ $i ] ) ) {
693 // Manually set some strings.
694 case 'NULL':
695 $sp_format = '';
696 break;
697 case 'boolean':
698 $sp_format = ( true === $work_array[ $i ] ) ? 'true' : 'false';
699 break;
700 // Make sure sprintf has a good datatype to work with.
701 case 'integer':
702 $sp_format = '%d';
703 break;
704 case 'double':
705 $sp_format = '%0.2f';
706 break;
707 case 'string':
708 $sp_format = '%s';
709 $work_array[ $i ] = str_replace( "$enclosure", "$enclosure$enclosure", $work_array[ $i ] );
710 break;
711 // Unknown or invalid items for a csv - note: the datatype of array is already handled above, assuming the data is nested.
712 case 'object':
713 case 'resource':
714 default:
715 $sp_format = '';
716 break;
717 }
718 $output .= sprintf( '%2$s' . $sp_format . '%2$s', $work_array[ $i ], $enclosure );
719 $output .= ( $i < ( $array_size - 1 ) ) ? $delimiter : $terminator;
720 }
721 }
722
723 return $output;
724 }
725
726 /**
727 * Resolve taxonomy slugs to term taxonomy IDs.
728 *
729 * @param array $settings The settings array containing the taxonomy slugs to sanitize.
730 * @param string $source_key The key in the settings array containing the slugs. Pattern is Name (taxonomy:term_taxonomy_id).
731 * @param string $target_key The key in the settings array to store the sanitized term taxonomy IDs.
732 * @return void
733 */
734 public static function sanitize_tax_slugs( &$settings, $source_key, $target_key ) {
735 if ( isset( $settings[ $source_key ] ) ) {
736 $slugs = array_unique( str_getcsv( $settings[ $source_key ], ',', '"', '' ) );
737
738 $tax_ids = array();
739 $tax_slugs = array();
740
741 foreach ( $slugs as $slug ) {
742 // Pattern is Name (taxonomy:term_taxonomy_id).
743 preg_match( '/(.*)\((.*):(\d+)\)/i', (string) $slug, $matches );
744 if ( isset( $matches[3] ) ) {
745 $term = get_term_by( 'term_taxonomy_id', $matches[3] );
746 } else {
747 // Fallback to fetching the category as this was the original format.
748 $term = get_term_by( 'name', $slug, 'category' );
749 }
750 if ( isset( $term->term_taxonomy_id ) ) {
751 $tax_ids[] = $term->term_taxonomy_id;
752 $tax_slugs[] = "{$term->name} ({$term->taxonomy}:{$term->term_taxonomy_id})";
753 }
754 }
755
756 $settings[ $target_key ] = join( ',', $tax_ids );
757 $settings[ $source_key ] = self::str_putcsv( $tax_slugs );
758 }
759 }
760 }
761