PluginProbe
WebberZone Top 10 — Popular Posts / 4.4.3
WebberZone Top 10 — Popular Posts v4.4.3
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.4.3, at includes/admin/settings/class-settings-sanitize.php

582 lines 16.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 * Miscellaneous sanitize function
78 *
79 * @param mixed $value Setting Value.
80 * @return string Sanitized value.
81 */
82 public function sanitize_missing( $value ) {
83 return $value;
84 }
85
86 /**
87 * Sanitize text fields
88 *
89 * @param string $value The field value.
90 * @return string Sanitizied value
91 */
92 public function sanitize_text_field( $value ) {
93 return $this->sanitize_textarea_field( $value );
94 }
95
96 /**
97 * Sanitize number fields
98 *
99 * @param string $value The field value.
100 * @return string Sanitized value
101 */
102 public function sanitize_number_field( $value ) {
103 return filter_var( $value, FILTER_SANITIZE_NUMBER_INT );
104 }
105
106 /**
107 * Sanitize CSV fields
108 *
109 * @param string $value The field value.
110 * @return string Sanitizied value
111 */
112 public function sanitize_csv_field( $value ) {
113 return implode( ',', array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) );
114 }
115
116 /**
117 * Sanitize CSV fields which hold numbers
118 *
119 * @param string $value The field value.
120 * @return string Sanitized value
121 */
122 public function sanitize_numbercsv_field( $value ) {
123 return implode( ',', array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) ) );
124 }
125
126 /**
127 * Sanitize CSV fields which hold post IDs
128 *
129 * @param string $value The field value.
130 * @return string Sanitized value
131 */
132 public function sanitize_postids_field( $value ) {
133 $ids = array_filter( array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $value ) ) ) ) );
134
135 foreach ( $ids as $key => $value ) {
136 if ( false === get_post_status( $value ) ) {
137 unset( $ids[ $key ] );
138 }
139 }
140
141 return implode( ',', $ids );
142 }
143
144 /**
145 * Sanitize textarea fields
146 *
147 * @param string $value The field value.
148 * @return string Sanitized value
149 */
150 public function sanitize_textarea_field( $value ) {
151
152 global $allowedposttags;
153
154 // We need more tags to allow for script and style.
155 $moretags = array(
156 'script' => array(
157 'type' => true,
158 'src' => true,
159 'async' => true,
160 'defer' => true,
161 'charset' => true,
162 ),
163 'style' => array(
164 'type' => true,
165 'media' => true,
166 'scoped' => true,
167 ),
168 'link' => array(
169 'rel' => true,
170 'type' => true,
171 'href' => true,
172 'media' => true,
173 'sizes' => true,
174 'hreflang' => true,
175 ),
176 );
177
178 $allowedtags = array_merge( $allowedposttags, $moretags );
179
180 /**
181 * Filter allowed tags allowed when sanitizing text and textarea fields.
182 *
183 * @param array $allowedtags Allowed tags array.
184 */
185 $allowedtags = apply_filters( $this->prefix . '_sanitize_allowed_tags', $allowedtags ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
186
187 return wp_kses( wp_unslash( $value ), $allowedtags );
188 }
189
190 /**
191 * Sanitize checkbox fields
192 *
193 * @param mixed $value The field value.
194 * @return int Sanitized value
195 */
196 public function sanitize_checkbox_field( $value ) {
197 $value = in_array( (int) $value, array( 0, -1 ), true ) ? 0 : 1;
198
199 return $value;
200 }
201
202 /**
203 * Sanitize toggle fields
204 *
205 * @param mixed $value The field value.
206 * @return int Sanitized value
207 */
208 public function sanitize_toggle_field( $value ) {
209 return $this->sanitize_checkbox_field( $value );
210 }
211
212 /**
213 * Sanitize multicheck fields
214 *
215 * @param array|int $value The field value.
216 * @return string $value Sanitized value
217 */
218 public function sanitize_multicheck_field( $value ) {
219 $values = ( -1 === (int) $value ) ? array() : array_map( 'sanitize_text_field', (array) wp_unslash( $value ) );
220
221 return implode( ',', $values );
222 }
223
224 /**
225 * Sanitize post_types fields
226 *
227 * @param array|int $value The field value.
228 * @return string $value Sanitized value
229 */
230 public function sanitize_posttypes_field( $value ) {
231 return $this->sanitize_multicheck_field( $value );
232 }
233
234 /**
235 * Sanitize post_types fields
236 *
237 * @param array|int $value The field value.
238 * @return string $value Sanitized value
239 */
240 public function sanitize_taxonomies_field( $value ) {
241 return $this->sanitize_multicheck_field( $value );
242 }
243
244 /**
245 * Sanitize color fields.
246 *
247 * @param string $value The field value.
248 * @return string Sanitized value
249 */
250 public function sanitize_color_field( $value ) {
251 return sanitize_hex_color( $value );
252 }
253
254 /**
255 * Sanitize email fields.
256 *
257 * @param string $value The field value.
258 * @return string Sanitized value
259 */
260 public function sanitize_email_field( $value ) {
261 return sanitize_email( $value );
262 }
263
264 /**
265 * Sanitize URL fields.
266 *
267 * @param string $value The field value.
268 * @return string Sanitized value
269 */
270 public function sanitize_url_field( $value ) {
271 return esc_url_raw( $value );
272 }
273
274 /**
275 * Sanitize sensitive fields.
276 *
277 * @param string $value The field value.
278 * @param string|array $key The field key.
279 * @return string Sanitized value
280 */
281 public function sanitize_sensitive_field( $value, $key ) {
282 if ( is_array( $key ) ) {
283 if ( isset( $key['id'] ) ) {
284 $key = $key['id'];
285 } else {
286 return $value;
287 }
288 }
289
290 $stored_encrypted_key = $this->get_option( $key );
291
292 // Empty input clears the stored value.
293 if ( '' === (string) $value ) {
294 return '';
295 }
296
297 // If input is masked, return existing encrypted key.
298 if ( strpos( (string) $value, '**' ) !== false ) {
299 return $stored_encrypted_key;
300 }
301
302 return Settings_API::encrypt_api_key( $value );
303 }
304
305 /**
306 * Sanitize repeater field.
307 *
308 * @param mixed $value Array of repeater values (may be non-array from form data).
309 * @param array $field Field configuration array.
310 * @return array Sanitized array
311 */
312 public function sanitize_repeater_field( $value, $field = array() ) {
313 // No usable controls are rendered, so submitted rows are forged.
314 if ( ! empty( $field['disabled'] ) || ! empty( $field['pro'] ) ) {
315 $stored = ! empty( $field['id'] ) ? $this->get_option( $field['id'], array() ) : array();
316 return is_array( $stored ) ? $stored : array();
317 }
318
319 if ( ! is_array( $value ) ) {
320 return array();
321 }
322
323 $sanitized_value = array();
324 $existing_rows = array();
325
326 // Get the subfields configuration.
327 $subfields = ! empty( $field['fields'] ) ? $field['fields'] : array();
328 if ( ! empty( $field['id'] ) ) {
329 $stored_value = $this->get_option( $field['id'], array() );
330 $existing_rows = is_array( $stored_value ) ? $stored_value : array();
331 }
332
333 // Create a lookup table for existing rows by row_id.
334 $existing_by_id = array();
335 foreach ( $existing_rows as $existing_row ) {
336 if ( isset( $existing_row['row_id'] ) ) {
337 $existing_by_id[ $existing_row['row_id'] ] = $existing_row;
338 }
339 }
340
341 foreach ( $value as $index => $row ) {
342 // Ensure we have a valid row structure.
343 if ( ! isset( $row['fields'] ) || ! is_array( $row['fields'] ) ) {
344 continue;
345 }
346
347 $sanitized_row = array(
348 'fields' => array(),
349 );
350
351 // Preserve row_id if it exists.
352 if ( isset( $row['row_id'] ) ) {
353 $sanitized_row['row_id'] = sanitize_text_field( $row['row_id'] );
354 }
355
356 // Get the corresponding existing row for sensitive field preservation.
357 $existing_row = null;
358 if ( isset( $row['row_id'] ) && isset( $existing_by_id[ $row['row_id'] ] ) ) {
359 $existing_row = $existing_by_id[ $row['row_id'] ];
360 }
361
362 foreach ( $row['fields'] as $field_key => $field_value ) {
363 $field_key = sanitize_key( $field_key );
364
365 // Skip if field_key is not in our subfields configuration.
366 $field_config = null;
367 foreach ( $subfields as $subfield ) {
368 if ( isset( $subfield['id'] ) && $subfield['id'] === $field_key ) {
369 $field_config = $subfield;
370 break;
371 }
372 }
373
374 if ( null === $field_config ) {
375 continue;
376 }
377
378 // Get the field type from the subfield configuration.
379 $field_type = isset( $field_config['type'] ) ? $field_config['type'] : 'text';
380
381 // For sensitive fields, distinguish empty (clear) from masked (preserve).
382 if ( 'sensitive' === $field_type ) {
383 if ( '' === (string) $field_value ) {
384 $sanitized_row['fields'][ $field_key ] = '';
385 continue;
386 }
387 if ( is_string( $field_value ) && false !== strpos( $field_value, '**' ) ) {
388 if ( $existing_row && isset( $existing_row['fields'][ $field_key ] ) ) {
389 $sanitized_row['fields'][ $field_key ] = $existing_row['fields'][ $field_key ];
390 }
391 continue;
392 }
393 }
394
395 // Call the appropriate sanitization method.
396 $sanitize_method = 'sanitize_' . $field_type . '_field';
397 if ( method_exists( $this, $sanitize_method ) ) {
398 if ( 'sensitive' === $field_type ) {
399 $sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_key );
400 } else {
401 $sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
402 }
403 } else {
404 $sanitized_row['fields'][ $field_key ] = $this->sanitize_text_field( $field_value );
405 }
406 }
407
408 if ( ! empty( $sanitized_row['fields'] ) ) {
409 $sanitized_value[ $index ] = $sanitized_row;
410 }
411 }
412
413 return $sanitized_value;
414 }
415
416 /**
417 * Find repeater rows that fail their own required-field rules.
418 *
419 * A subfield is required when its own config sets `required => true`. A repeater
420 * can additionally set `required_one_of => array( subfield_id, ... )` on itself to
421 * require at least one of several alternative subfields per row (e.g. a post OR a
422 * URL). Purely structural - it returns what is wrong, not a human message, so it
423 * carries no i18n and can be reused unchanged by any plugin that copies this file.
424 *
425 * @param array $rows Sanitized repeater rows, as returned by sanitize_repeater_field().
426 * @param array $field Repeater field configuration.
427 * @return array Map of row index => issue, where issue may have a 'missing' key
428 * (subfield IDs with required => true that are empty) and/or a
429 * 'missing_one_of' key (the required_one_of group, present only when
430 * none of it is filled).
431 */
432 public static function get_incomplete_repeater_rows( array $rows, array $field ) {
433 $subfields = ! empty( $field['fields'] ) && is_array( $field['fields'] ) ? $field['fields'] : array();
434
435 $required_subfields = array();
436 foreach ( $subfields as $subfield_id => $subfield ) {
437 // Row values are keyed by the subfield's own id; the array key may be numeric.
438 if ( ! empty( $subfield['required'] ) ) {
439 $required_subfields[] = $subfield['id'] ?? $subfield_id;
440 }
441 }
442
443 $required_one_of = ! empty( $field['required_one_of'] ) && is_array( $field['required_one_of'] ) ? $field['required_one_of'] : array();
444
445 if ( empty( $required_subfields ) && empty( $required_one_of ) ) {
446 return array();
447 }
448
449 $is_filled = static function ( $values, $subfield_id ) {
450 return '' !== trim( (string) ( $values[ $subfield_id ] ?? '' ), " \t\n\r\0\x0B," );
451 };
452 $incomplete = array();
453
454 foreach ( array_values( $rows ) as $index => $row ) {
455 $values = isset( $row['fields'] ) && is_array( $row['fields'] ) ? $row['fields'] : array();
456 $issue = array();
457
458 $missing = array();
459 foreach ( $required_subfields as $subfield_id ) {
460 if ( ! $is_filled( $values, $subfield_id ) ) {
461 $missing[] = $subfield_id;
462 }
463 }
464 if ( ! empty( $missing ) ) {
465 $issue['missing'] = $missing;
466 }
467
468 if ( ! empty( $required_one_of ) ) {
469 $filled = false;
470 foreach ( $required_one_of as $subfield_id ) {
471 if ( $is_filled( $values, $subfield_id ) ) {
472 $filled = true;
473 break;
474 }
475 }
476 if ( ! $filled ) {
477 $issue['missing_one_of'] = $required_one_of;
478 }
479 }
480
481 if ( ! empty( $issue ) ) {
482 $incomplete[ $index ] = $issue;
483 }
484 }
485
486 return $incomplete;
487 }
488
489 /**
490 * Convert a string to CSV.
491 *
492 * @param array $input_array Input string.
493 * @param string $delimiter Delimiter.
494 * @param string $enclosure Enclosure.
495 * @param string $terminator Terminating string.
496 * @return string CSV string.
497 */
498 public static function str_putcsv( $input_array, $delimiter = ',', $enclosure = '"', $terminator = "\n" ) {
499 // First convert associative array to numeric indexed array.
500 $work_array = array();
501 foreach ( $input_array as $key => $value ) {
502 $work_array[] = $value;
503 }
504
505 $output = '';
506 $array_size = count( $work_array );
507
508 for ( $i = 0; $i < $array_size; $i++ ) {
509 // Nested array, process nest item.
510 if ( is_array( $work_array[ $i ] ) ) {
511 $output .= self::str_putcsv( $work_array[ $i ], $delimiter, $enclosure, $terminator );
512 } else {
513 switch ( gettype( $work_array[ $i ] ) ) {
514 // Manually set some strings.
515 case 'NULL':
516 $sp_format = '';
517 break;
518 case 'boolean':
519 $sp_format = ( true === $work_array[ $i ] ) ? 'true' : 'false';
520 break;
521 // Make sure sprintf has a good datatype to work with.
522 case 'integer':
523 $sp_format = '%d';
524 break;
525 case 'double':
526 $sp_format = '%0.2f';
527 break;
528 case 'string':
529 $sp_format = '%s';
530 $work_array[ $i ] = str_replace( "$enclosure", "$enclosure$enclosure", $work_array[ $i ] );
531 break;
532 // Unknown or invalid items for a csv - note: the datatype of array is already handled above, assuming the data is nested.
533 case 'object':
534 case 'resource':
535 default:
536 $sp_format = '';
537 break;
538 }
539 $output .= sprintf( '%2$s' . $sp_format . '%2$s', $work_array[ $i ], $enclosure );
540 $output .= ( $i < ( $array_size - 1 ) ) ? $delimiter : $terminator;
541 }
542 }
543
544 return $output;
545 }
546
547 /**
548 * Processes category/taxonomy slugs and adds a new element to the settings array containing the term taxonomy IDs.
549 *
550 * @param array $settings The settings array containing the taxonomy slugs to sanitize.
551 * @param string $source_key The key in the settings array containing the slugs. Pattern is Name (taxonomy:term_taxonomy_id).
552 * @param string $target_key The key in the settings array to store the sanitized term taxonomy IDs.
553 * @return void
554 */
555 public static function sanitize_tax_slugs( &$settings, $source_key, $target_key ) {
556 if ( isset( $settings[ $source_key ] ) ) {
557 $slugs = array_unique( str_getcsv( $settings[ $source_key ], ',', '"', '' ) );
558
559 $tax_ids = array();
560 $tax_slugs = array();
561
562 foreach ( $slugs as $slug ) {
563 // Pattern is Name (taxonomy:term_taxonomy_id).
564 preg_match( '/(.*)\((.*):(\d+)\)/i', (string) $slug, $matches );
565 if ( isset( $matches[3] ) ) {
566 $term = get_term_by( 'term_taxonomy_id', $matches[3] );
567 } else {
568 // Fallback to fetching the category as this was the original format.
569 $term = get_term_by( 'name', $slug, 'category' );
570 }
571 if ( isset( $term->term_taxonomy_id ) ) {
572 $tax_ids[] = $term->term_taxonomy_id;
573 $tax_slugs[] = "{$term->name} ({$term->taxonomy}:{$term->term_taxonomy_id})";
574 }
575 }
576
577 $settings[ $target_key ] = join( ',', $tax_ids );
578 $settings[ $source_key ] = self::str_putcsv( $tax_slugs );
579 }
580 }
581 }
582