PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.1
WebberZone Top 10 — Popular Posts v4.5.1
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 1.6.1 All 117 releases
top-10 / includes / admin / settings / class-settings-sanitize.php

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

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