PluginProbe
WebberZone Top 10 — Popular Posts / 4.3.4
WebberZone Top 10 — Popular Posts v4.3.4
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-form.php

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

1,201 lines 36.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Generates the settings form.
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 * Generates the settings form.
19 */
20 class Settings_Form {
21
22
23 /**
24 * Settings Key.
25 *
26 * @var string Settings Key.
27 */
28 public $settings_key;
29
30 /**
31 * Prefix which is used for creating the unique filters and actions.
32 *
33 * @var string Prefix.
34 */
35 public $prefix;
36
37 /**
38 * Translation strings.
39 *
40 * @var array Translation strings.
41 */
42 public $translation_strings;
43
44 /**
45 * Main constructor class.
46 *
47 * @param mixed $args {
48 * Array or string of arguments. Default is blank array.
49 * @type string $settings_key Settings key.
50 * @type string $prefix Prefix.
51 * @type array $translation_strings Translation strings.
52 * }
53 */
54 public function __construct( $args ) {
55 $defaults = array(
56 'settings_key' => '',
57 'prefix' => '',
58 'translation_strings' => array(),
59 );
60 $args = wp_parse_args( $args, $defaults );
61
62 foreach ( $args as $name => $value ) {
63 $this->$name = $value;
64 }
65 }
66
67 /**
68 * Get field description for display.
69 *
70 * @param array $args settings Arguments array.
71 *
72 * @return string Description of the field.
73 */
74 public function get_field_description( $args ) {
75 $desc = ! empty( $args['desc'] ) ? '<p class="description">' . wp_kses_post( $args['desc'] ) . '</p>' : '';
76
77 /**
78 * After Settings Output filter
79 *
80 * @param string $desc Description of the field.
81 * @param array $args Arguments array.
82 */
83 $desc = apply_filters( $this->prefix . '_setting_field_description', $desc, $args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
84
85 return $desc;
86 }
87
88 /**
89 * Get the value of a settings field.
90 *
91 * @param string $option Settings field name.
92 * @param mixed $default_value Default value if option is not found.
93 * @return mixed
94 */
95 public function get_option( $option, $default_value = '' ) {
96
97 $options = \get_option( $this->settings_key );
98
99 if ( isset( $options[ $option ] ) ) {
100 return $options[ $option ];
101 }
102
103 return $default_value;
104 }
105
106 /**
107 * Get field value from args or options.
108 *
109 * @param array $args Field arguments.
110 * @return mixed Field value.
111 */
112 protected function get_field_value( $args ) {
113 return $args['value'] ?? $this->get_option( $args['id'], $args['default'] ?? '' );
114 }
115
116 /**
117 * Get sanitized field class string.
118 *
119 * @param array $args Field arguments.
120 * @param string $default_class Default class to prepend.
121 * @return string Sanitized class string.
122 */
123 protected function get_field_class( $args, $default_class = '' ) {
124 $class = implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $args['field_class'] ?? '' ) ) );
125 if ( $default_class ) {
126 $class = $default_class . ' ' . $class;
127 }
128 return trim( $class );
129 }
130
131 /**
132 * Get placeholder attribute string.
133 *
134 * @param array $args Field arguments.
135 * @return string Placeholder attribute or empty string.
136 */
137 protected function get_placeholder_attribute( $args ) {
138 return empty( $args['placeholder'] ) ? '' : ' placeholder="' . esc_attr( $args['placeholder'] ) . '"';
139 }
140
141 /**
142 * Get boolean state attributes (disabled, readonly, required).
143 *
144 * @param array $args Field arguments.
145 * @return string Concatenated boolean attributes.
146 */
147 protected function get_boolean_attributes( $args ) {
148 $disabled = ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : '';
149 $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : '';
150 $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
151 return $disabled . $readonly . $required;
152 }
153
154 /**
155 * Get field ID and name attributes.
156 *
157 * @param array $args Field arguments.
158 * @return array Array containing field_id and field_name.
159 */
160 protected function get_field_attributes( $args ) {
161 $id = sanitize_key( $args['id'] );
162 if ( isset( $args['_repeater_id'] ) && isset( $args['_index'] ) ) {
163 $field_id = sprintf(
164 '%s-%s-%s-fields-%s',
165 $this->settings_key,
166 $args['_repeater_id'],
167 $args['_index'],
168 $id
169 );
170 $field_name = sprintf(
171 '%s[%s][%s][fields][%s]',
172 $this->settings_key,
173 $args['_repeater_id'],
174 $args['_index'],
175 $id
176 );
177 } else {
178 $field_id = $this->settings_key . '-' . $id;
179 $field_name = $this->settings_key . '[' . $id . ']';
180 }
181
182 return array(
183 'field_id' => $field_id,
184 'field_name' => $field_name,
185 );
186 }
187
188 /**
189 * Returns the allowed HTML tags and attributes for settings form output.
190 *
191 * Use the `{prefix}_settings_form_allowed_html` filter to add extra tags or
192 * attributes needed by custom field types or `field_attributes` entries.
193 *
194 * @return array<string, array<string, bool>>
195 */
196 protected function get_allowed_html(): array {
197 $allowed = wp_kses_allowed_html( 'post' );
198
199 $form_tags = array(
200 'input' => array(
201 'type' => true,
202 'name' => true,
203 'id' => true,
204 'value' => true,
205 'class' => true,
206 'style' => true,
207 'checked' => true,
208 'disabled' => true,
209 'readonly' => true,
210 'required' => true,
211 'placeholder' => true,
212 'size' => true,
213 'min' => true,
214 'max' => true,
215 'step' => true,
216 'multiple' => true,
217 'autocomplete' => true,
218 // Tom Select data attributes (built-in Settings API feature).
219 'data-wp-prefix' => true,
220 'data-wp-action' => true,
221 'data-wp-nonce' => true,
222 'data-wp-endpoint' => true,
223 'data-ts-config' => true,
224 'data-wp-extra-fields' => true,
225 ),
226 'select' => array(
227 'id' => true,
228 'name' => true,
229 'class' => true,
230 'style' => true,
231 'disabled' => true,
232 'multiple' => true,
233 'size' => true,
234 'autocomplete' => true,
235 // Tom Select data attributes (built-in Settings API feature).
236 'data-wp-prefix' => true,
237 'data-wp-action' => true,
238 'data-wp-nonce' => true,
239 'data-wp-endpoint' => true,
240 'data-ts-config' => true,
241 'data-wp-extra-fields' => true,
242 ),
243 'option' => array(
244 'value' => true,
245 'selected' => true,
246 'disabled' => true,
247 ),
248 'optgroup' => array(
249 'label' => true,
250 'disabled' => true,
251 ),
252 'textarea' => array(
253 'id' => true,
254 'name' => true,
255 'class' => true,
256 'style' => true,
257 'rows' => true,
258 'cols' => true,
259 'disabled' => true,
260 'readonly' => true,
261 'required' => true,
262 'placeholder' => true,
263 'autocomplete' => true,
264 ),
265 'label' => array(
266 'for' => true,
267 'class' => true,
268 'style' => true,
269 ),
270 'button' => array(
271 'type' => true,
272 'id' => true,
273 'class' => true,
274 'style' => true,
275 'disabled' => true,
276 ),
277 'template' => array(
278 'class' => true,
279 'data-id' => true,
280 ),
281 );
282
283 $allowed = array_replace_recursive( $allowed, $form_tags );
284
285 /**
286 * Filters the allowed HTML tags and attributes for settings form output.
287 *
288 * Use this filter to add data attributes or custom elements required by
289 * field_attributes entries in your registered settings.
290 *
291 * @param array<string, array<string, bool>> $allowed Allowed HTML tags/attributes.
292 */
293 return apply_filters( $this->prefix . '_settings_form_allowed_html', $allowed ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
294 }
295
296 /**
297 * Miscellaneous callback funcion
298 *
299 * @param array $args Arguments array.
300 * @return void
301 */
302 public function callback_missing( $args ) {
303 /* translators: 1: Code. */
304 printf( 'The callback function used for the %1$s setting is missing.', '<strong>' . esc_attr( $args['id'] ) . '</strong>' );
305 }
306
307 /**
308 * Header Callback
309 *
310 * Renders the header.
311 *
312 * @param array $args Arguments passed by the setting.
313 * @return void
314 */
315 public function callback_header( $args ) {
316 $html = $this->get_field_description( $args );
317
318 /**
319 * After Settings Output filter
320 *
321 * @param string $html HTML string.
322 * @param array $args Arguments array.
323 */
324 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
325 }
326
327 /**
328 * Descriptive text callback.
329 *
330 * Renders descriptive text onto the settings field.
331 *
332 * @param array $args Array of arguments.
333 * @return void
334 */
335 public function callback_descriptive_text( $args ) {
336 $this->callback_header( $args );
337 }
338
339 /**
340 * Build additional attributes string from field_attributes.
341 *
342 * @param array $args Field arguments.
343 * @return string Additional attributes string.
344 */
345 protected function build_field_attributes( $args ) {
346 $attributes = '';
347 foreach ( (array) ( $args['field_attributes'] ?? array() ) as $attribute => $val ) {
348 $attr_name = sanitize_key( $attribute );
349 if ( empty( $attr_name ) ) {
350 continue;
351 }
352 if ( true === $val ) {
353 $attributes .= ' ' . $attr_name;
354 } elseif ( false !== $val ) {
355 $attributes .= sprintf( ' %1$s="%2$s"', $attr_name, esc_attr( $val ) );
356 }
357 }
358 return $attributes;
359 }
360
361 /**
362 * Display text fields.
363 *
364 * @param array $args Array of arguments.
365 */
366 public function callback_text( $args ) {
367 $value = $this->get_field_value( $args );
368 $size = sanitize_html_class( $args['size'] ?? 'regular' );
369 $class = $this->get_field_class( $args );
370 $placeholder = $this->get_placeholder_attribute( $args );
371 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
372
373 $field_attributes = $this->get_field_attributes( $args );
374
375 $html = sprintf(
376 '<input type="text" id="%1$s" name="%2$s" class="%3$s" value="%4$s" %5$s %6$s />',
377 $field_attributes['field_id'],
378 $field_attributes['field_name'],
379 $class . ' ' . $size . '-text',
380 esc_attr( stripslashes( $value ) ),
381 $attributes,
382 $placeholder
383 );
384 $html .= $this->get_field_description( $args );
385
386 /**
387 * This filter has been defined in class-settings-api.php
388 */
389 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
390 }
391
392 /**
393 * Display url fields.
394 *
395 * @param array $args Array of arguments.
396 */
397 public function callback_url( $args ) {
398 $this->callback_text( $args );
399 }
400
401 /**
402 * Display csv fields.
403 *
404 * @param array $args Array of arguments.
405 */
406 public function callback_csv( $args ) {
407 $this->callback_text( $args );
408 }
409
410 /**
411 * Display color fields.
412 *
413 * @param array $args Array of arguments.
414 */
415 public function callback_color( $args ) {
416 // Add color-field class for wpColorPicker initialization.
417 $args['field_class'] = ! empty( $args['field_class'] ) ? $args['field_class'] . ' color-field' : 'color-field';
418 $this->callback_text( $args );
419 }
420
421 /**
422 * Display numbercsv fields.
423 *
424 * @param array $args Array of arguments.
425 */
426 public function callback_numbercsv( $args ) {
427 $this->callback_text( $args );
428 }
429
430 /**
431 * Display postids fields.
432 *
433 * @param array $args Array of arguments.
434 */
435 public function callback_postids( $args ) {
436 $this->callback_text( $args );
437 }
438
439 /**
440 * Display textarea.
441 *
442 * @param array $args Array of arguments.
443 * @return void
444 */
445 public function callback_textarea( $args ) {
446 $value = $this->get_field_value( $args );
447 $class = $this->get_field_class( $args, 'large-text' );
448 $placeholder = $this->get_placeholder_attribute( $args );
449 $attributes = $this->get_boolean_attributes( $args );
450
451 $field_attributes = $this->get_field_attributes( $args );
452
453 $html = sprintf(
454 '<textarea class="%4$s" cols="50" rows="5" id="%1$s" name="%2$s" %5$s %6$s>%3$s</textarea>',
455 $field_attributes['field_id'],
456 $field_attributes['field_name'],
457 esc_textarea( stripslashes( $value ) ),
458 $class,
459 $attributes,
460 $placeholder
461 );
462 $html .= $this->get_field_description( $args );
463
464 /**
465 * This filter has been defined in class-settings-api.php
466 */
467 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
468 }
469
470 /**
471 * Display CSS fields.
472 *
473 * @param array $args Array of arguments.
474 * @return void
475 */
476 public function callback_css( $args ) {
477 $this->callback_textarea( $args );
478 }
479
480 /**
481 * Display HTML fields.
482 *
483 * @param array $args Array of arguments.
484 * @return void
485 */
486 public function callback_html( $args ) {
487 $this->callback_textarea( $args );
488 }
489
490 /**
491 * Get disabled attribute for pro/premium fields.
492 *
493 * @param array $args Field arguments.
494 * @return string Disabled attribute or empty string.
495 */
496 protected function get_disabled_attribute( $args ) {
497 return ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : '';
498 }
499
500 /**
501 * Display checkboxes.
502 *
503 * @param array $args Array of arguments.
504 * @return void
505 */
506 public function callback_checkbox( $args ) {
507 $value = $this->get_field_value( $args );
508 $checked = ! empty( $value ) ? checked( 1, $value, false ) : '';
509 $default = isset( $args['default'] ) ? (int) $args['default'] : '';
510 $disabled = $this->get_disabled_attribute( $args );
511
512 $field_attributes = $this->get_field_attributes( $args );
513
514 $html = sprintf(
515 '<input type="hidden" name="%1$s" value="-1" />',
516 $field_attributes['field_name']
517 );
518 $html .= sprintf(
519 '<input type="checkbox" id="%1$s" name="%2$s" value="1" %3$s %4$s />',
520 $field_attributes['field_id'],
521 $field_attributes['field_name'],
522 $checked,
523 $disabled
524 );
525 $checkbox_modified = $this->translation_strings['checkbox_modified'] ?? 'Modified from default setting';
526 $html .= ( (bool) $value !== (bool) $default ) ? '<em style="color:#9B0800">' . $checkbox_modified . '</em>' : '';
527 $html .= $this->get_field_description( $args );
528
529 /**
530 * This filter has been defined in class-settings-api.php
531 */
532 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
533 }
534
535 /**
536 * Multicheck Callback
537 *
538 * Renders multiple checkboxes.
539 *
540 * @param array $args Array of arguments.
541 * @return void
542 */
543 public function callback_multicheck( $args ) {
544 $html = '';
545
546 $value = $this->get_field_value( $args );
547 $value_array = wp_parse_list( $value );
548 $disabled = $this->get_disabled_attribute( $args );
549
550 $field_attributes = $this->get_field_attributes( $args );
551
552 if ( ! empty( $args['options'] ) ) {
553 $html .= sprintf(
554 '<input type="hidden" name="%1$s" value="-1" />',
555 $field_attributes['field_name']
556 );
557
558 foreach ( $args['options'] as $key => $option ) {
559 if ( in_array( $key, $value_array, true ) ) {
560 $enabled = $key;
561 } else {
562 $enabled = null;
563 }
564
565 $option_id = $field_attributes['field_id'] . '-' . sanitize_key( $key );
566 $option_name = $field_attributes['field_name'] . '[' . sanitize_key( $key ) . ']';
567
568 $html .= sprintf(
569 '<input name="%1$s" id="%2$s" type="checkbox" value="%3$s" %4$s %5$s /> ',
570 $option_name,
571 $option_id,
572 esc_attr( $key ),
573 checked( $key, $enabled, false ),
574 $disabled
575 );
576 $html .= sprintf(
577 '<label for="%1$s">%2$s</label> <br />',
578 $option_id,
579 wp_kses_post( $option )
580 );
581 }
582 }
583 $html .= $this->get_field_description( $args );
584
585 /**
586 * This filter has been defined in class-settings-api.php
587 */
588 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
589 }
590
591 /**
592 * Radio Callback
593 *
594 * Renders radio boxes.
595 *
596 * @param array $args Array of arguments.
597 * @return void
598 */
599 public function callback_radio( $args ) {
600 $html = '';
601
602 $value = $this->get_field_value( $args );
603 $disabled = $this->get_disabled_attribute( $args );
604
605 $field_attributes = $this->get_field_attributes( $args );
606
607 foreach ( $args['options'] as $key => $option ) {
608 $option_id = $field_attributes['field_id'] . '-' . $key;
609
610 $html .= sprintf(
611 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s %5$s /> ',
612 $field_attributes['field_name'],
613 $option_id,
614 $key,
615 checked( $value, $key, false ),
616 $disabled
617 );
618 $html .= sprintf(
619 '<label for="%1$s">%2$s</label> <br />',
620 $option_id,
621 wp_kses_post( $option )
622 );
623 }
624
625 $html .= $this->get_field_description( $args );
626
627 /**
628 * This filter has been defined in class-settings-api.php
629 */
630 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
631 }
632
633 /**
634 * Radio callback with description.
635 *
636 * Renders radio boxes with each item having it separate description.
637 *
638 * @param array $args Array of arguments.
639 * @return void
640 */
641 public function callback_radiodesc( $args ) {
642 $html = '';
643
644 $value = $this->get_field_value( $args );
645 $disabled = $this->get_disabled_attribute( $args );
646
647 $field_attributes = $this->get_field_attributes( $args );
648
649 foreach ( $args['options'] as $option ) {
650 $option_id = $field_attributes['field_id'] . '-' . $option['id'];
651
652 $html .= sprintf(
653 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s %5$s /> ',
654 $field_attributes['field_name'],
655 $option_id,
656 $option['id'],
657 checked( $value, $option['id'], false ),
658 $disabled
659 );
660 $html .= sprintf(
661 '<label for="%1$s">%2$s: <em>%3$s</em></label>',
662 $option_id,
663 wp_kses_post( $option['name'] ),
664 wp_kses_post( $option['description'] )
665 );
666
667 $html .= '<br />';
668 }
669
670 $html .= $this->get_field_description( $args );
671
672 /**
673 * This filter has been defined in class-settings-api.php
674 */
675 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
676 }
677
678 /**
679 * Radio callback with description.
680 *
681 * Renders radio boxes with each item having it separate description.
682 *
683 * @param array $args Array of arguments.
684 * @return void
685 */
686 public function callback_thumbsizes( $args ) {
687 $html = '';
688
689 $thumb_size = $this->prefix . '_thumbnail';
690
691 if ( ! isset( $args['options'][ $thumb_size ] ) ) {
692 $args['options'][ $thumb_size ] = array(
693 'name' => $thumb_size,
694 'width' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_width', 150 ) ),
695 'height' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_height', 150 ) ),
696 'crop' => call_user_func_array( $this->prefix . '_get_option', array( 'thumb_crop', true ) ),
697 );
698 }
699
700 $value = $this->get_field_value( $args );
701
702 $field_attributes = $this->get_field_attributes( $args );
703
704 foreach ( $args['options'] as $name => $option ) {
705 $option_id = $field_attributes['field_id'] . '-' . $name;
706
707 $html .= sprintf(
708 '<input name="%1$s" id="%2$s" type="radio" value="%3$s" %4$s /> ',
709 $field_attributes['field_name'],
710 $option_id,
711 $name,
712 checked( $value, $name, false )
713 );
714 $html .= sprintf(
715 '<label for="%1$s">%2$s (%3$sx%4$s%5$s)</label> <br />',
716 $option_id,
717 wp_kses_post( $name ),
718 (int) $option['width'],
719 (int) $option['height'],
720 (bool) $option['crop'] ? ' cropped' : ''
721 );
722 }
723
724 $html .= $this->get_field_description( $args );
725
726 /**
727 * This filter has been defined in class-settings-api.php
728 */
729 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
730 }
731
732 /**
733 * Number Callback
734 *
735 * Renders number fields.
736 *
737 * @param array $args Array of arguments.
738 * @return void
739 */
740 public function callback_number( $args ) {
741 $value = $this->get_field_value( $args );
742 $max = isset( $args['max'] ) ? intval( $args['max'] ) : 999999;
743 $min = isset( $args['min'] ) ? intval( $args['min'] ) : 0;
744 $step = isset( $args['step'] ) ? intval( $args['step'] ) : 1;
745 $size = $args['size'] ?? 'regular';
746 $placeholder = $this->get_placeholder_attribute( $args );
747 $attributes = $this->get_boolean_attributes( $args );
748
749 $field_attributes = $this->get_field_attributes( $args );
750
751 $html = sprintf(
752 '<input type="number" step="%1$s" max="%2$s" min="%3$s" class="%4$s" id="%5$s" name="%6$s" value="%7$s" %8$s %9$s />',
753 esc_attr( (string) $step ),
754 esc_attr( (string) $max ),
755 esc_attr( (string) $min ),
756 sanitize_html_class( $size ) . '-text',
757 $field_attributes['field_id'],
758 $field_attributes['field_name'],
759 esc_attr( stripslashes( $value ) ),
760 $placeholder,
761 $attributes
762 );
763 $html .= $this->get_field_description( $args );
764
765 /**
766 * This filter has been defined in class-settings-api.php
767 */
768 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
769 }
770
771 /**
772 * Select Callback
773 *
774 * Renders select fields.
775 *
776 * @param array $args Array of arguments.
777 * @return void
778 */
779 public function callback_select( $args ) {
780 $value = $this->get_field_value( $args );
781 $class = $this->get_field_class( $args );
782 $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : '';
783 $attributes = $this->get_disabled_attribute( $args ) . $required . $this->build_field_attributes( $args );
784
785 if ( isset( $args['chosen'] ) ) {
786 $class .= ' chosen';
787 }
788
789 $field_attributes = $this->get_field_attributes( $args );
790
791 $html = sprintf(
792 '<select id="%1$s" name="%2$s" class="%3$s" %4$s />',
793 $field_attributes['field_id'],
794 $field_attributes['field_name'],
795 $class,
796 $attributes
797 );
798
799 foreach ( (array) $args['options'] as $option => $name ) {
800 $html .= sprintf( '<option value="%1$s" %2$s>%3$s</option>', sanitize_key( $option ), selected( $option, $value, false ), esc_html( $name ) );
801 }
802
803 $html .= '</select>';
804 $html .= $this->get_field_description( $args );
805
806 /**
807 * This filter has been defined in class-settings-api.php
808 */
809 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
810 }
811
812 /**
813 * Display posttypes fields.
814 *
815 * @param array $args Array of arguments.
816 * @return void
817 */
818 public function callback_posttypes( $args ) {
819 $html = '';
820
821 $options = $this->get_field_value( $args );
822 $disabled = $this->get_disabled_attribute( $args );
823
824 // If post_types contains a query string then parse it with wp_parse_args.
825 if ( is_string( $options ) && strpos( $options, '=' ) ) {
826 $post_types = wp_parse_args( $options );
827 } else {
828 $post_types = wp_parse_list( $options );
829 }
830
831 $wp_post_types = get_post_types(
832 array(
833 'public' => true,
834 ),
835 'objects'
836 );
837
838 $posts_types_inc = array_intersect( wp_list_pluck( $wp_post_types, 'name' ), $post_types );
839
840 $field_attributes = $this->get_field_attributes( $args );
841
842 $html .= sprintf(
843 '<input type="hidden" name="%1$s" value="-1" />',
844 $field_attributes['field_name']
845 );
846
847 foreach ( $wp_post_types as $wp_post_type ) {
848 $option_id = $field_attributes['field_id'] . '-' . esc_attr( $wp_post_type->name );
849 $option_name = $field_attributes['field_name'] . '[' . esc_attr( $wp_post_type->name ) . ']';
850
851 $html .= sprintf(
852 '<label for="%1$s"><input name="%2$s" id="%1$s" type="checkbox" value="%3$s" %4$s %5$s /> %6$s</label><br />',
853 $option_id,
854 $option_name,
855 esc_attr( $wp_post_type->name ),
856 checked( true, in_array( $wp_post_type->name, $posts_types_inc, true ), false ),
857 $disabled,
858 esc_html( $wp_post_type->label )
859 );
860
861 }
862
863 $html .= $this->get_field_description( $args );
864
865 /**
866 * This filter has been defined in class-settings-api.php
867 */
868 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
869 }
870
871
872 /**
873 * Display taxonomies fields.
874 *
875 * @param array $args Array of arguments.
876 * @return void
877 */
878 public function callback_taxonomies( $args ) {
879 $html = '';
880
881 $options = $this->get_field_value( $args );
882
883 // If taxonomies contains a query string then parse it with wp_parse_args.
884 if ( is_string( $options ) && strpos( $options, '=' ) ) {
885 $taxonomies = wp_parse_args( $options );
886 } else {
887 $taxonomies = wp_parse_list( $options );
888 }
889
890 /* Fetch taxonomies */
891 $argsc = array(
892 'public' => true,
893 );
894 $output = 'objects';
895 $operator = 'and';
896 $wp_taxonomies = get_taxonomies( $argsc, $output, $operator );
897
898 $taxonomies_inc = array_intersect( wp_list_pluck( (array) $wp_taxonomies, 'name' ), $taxonomies );
899
900 $field_attributes = $this->get_field_attributes( $args );
901
902 $html .= sprintf( '<input type="hidden" name="%1$s" value="-1" />', $field_attributes['field_name'] );
903
904 foreach ( $wp_taxonomies as $wp_taxonomy ) {
905 $option_id = $field_attributes['field_id'] . '-' . esc_attr( $wp_taxonomy->name );
906 $option_name = $field_attributes['field_name'] . '[' . esc_attr( $wp_taxonomy->name ) . ']';
907
908 $html .= sprintf(
909 '<label for="%1$s"><input name="%2$s" id="%1$s" type="checkbox" value="%3$s" %4$s /> %5$s (%3$s)</label><br />',
910 $option_id,
911 $option_name,
912 esc_attr( $wp_taxonomy->name ),
913 checked( true, in_array( $wp_taxonomy->name, $taxonomies_inc, true ), false ),
914 esc_html( $wp_taxonomy->labels->name )
915 );
916
917 }
918
919 $html .= $this->get_field_description( $args );
920
921 /**
922 * This filter has been defined in class-settings-api.php
923 */
924 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
925 }
926
927
928 /**
929 * Displays a rich text textarea for a settings field.
930 *
931 * @param array $args Array of arguments.
932 */
933 public function callback_wysiwyg( $args ) {
934 $value = $this->get_field_value( $args );
935 $size = $args['size'] ?? '500px';
936
937 $field_attributes = $this->get_field_attributes( $args );
938
939 // wp_editor requires a unique ID without brackets.
940 $editor_id = sanitize_key( str_replace( array( '[', ']' ), array( '-', '' ), $field_attributes['field_id'] ) );
941
942 printf( '<div style="max-width: %1$s;">', esc_attr( $size ) );
943
944 $editor_settings = array(
945 'teeny' => true,
946 'textarea_name' => $field_attributes['field_name'],
947 'textarea_rows' => 10,
948 );
949
950 if ( isset( $args['options'] ) && is_array( $args['options'] ) ) {
951 $editor_settings = array_merge( $editor_settings, $args['options'] );
952 }
953
954 wp_editor( $value, $editor_id, $editor_settings );
955
956 printf( '</div>' );
957
958 echo wp_kses_post( $this->get_field_description( $args ) );
959 }
960
961 /**
962 * Displays a file upload field for a settings field.
963 *
964 * @param array $args Array of arguments.
965 */
966 public function callback_file( $args ) {
967 $value = $this->get_field_value( $args );
968 $size = sanitize_html_class( $args['size'] ?? 'regular' );
969 $class = $this->get_field_class( $args );
970 $label = $args['options']['button_label'] ?? $this->translation_strings['button_label'];
971
972 $field_attributes = $this->get_field_attributes( $args );
973
974 $html = sprintf(
975 '<input type="text" class="%1$s" id="%2$s" name="%3$s" value="%4$s"/>',
976 $class . ' ' . $size . '-text file-url',
977 $field_attributes['field_id'],
978 $field_attributes['field_name'],
979 esc_attr( $value )
980 );
981 $html .= sprintf( '<input type="button" class="button button-secondary file-browser" value="%s" />', esc_attr( $label ) );
982 $html .= $this->get_field_description( $args );
983
984 /**
985 * This filter has been defined in class-settings-api.php
986 */
987 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
988 }
989
990 /**
991 * Displays a password field for a settings field.
992 *
993 * @param array $args Array of arguments.
994 */
995 public function callback_password( $args ) {
996 $value = $this->get_field_value( $args );
997 $size = sanitize_html_class( $args['size'] ?? 'regular' );
998 $class = $this->get_field_class( $args );
999
1000 $field_attributes = $this->get_field_attributes( $args );
1001
1002 $html = sprintf(
1003 '<input type="password" class="%1$s" id="%2$s" name="%3$s" value="%4$s" %5$s />',
1004 "$class $size-text",
1005 $field_attributes['field_id'],
1006 $field_attributes['field_name'],
1007 esc_attr( $value ),
1008 ! empty( $value ) ? 'placeholder="' . esc_attr( $this->translation_strings['previous_saved'] ) . '"' : ''
1009 );
1010 $html .= $this->get_field_description( $args );
1011
1012 /**
1013 * This filter has been defined in class-settings-api.php
1014 */
1015 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1016 }
1017
1018 /**
1019 * Callback for repeater field.
1020 *
1021 * @param array $args Array of arguments.
1022 * @return void
1023 */
1024 public function callback_repeater( $args ) {
1025 $raw_value = $this->get_field_value( $args );
1026 $value = ! empty( $raw_value ) && is_array( $raw_value ) ? $raw_value : array();
1027
1028 $class = $this->get_field_class( $args );
1029 $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args );
1030
1031 $data_index = (string) count( $value );
1032 $live_update_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1033 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1034 $live_update_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1035
1036 ob_start();
1037 ?>
1038 <div class="<?php echo esc_attr( $class ); ?> wz-repeater-wrapper"
1039 id="<?php echo esc_attr( $args['id'] ); ?>-wrapper"
1040 data-index="<?php echo esc_attr( $data_index ); ?>"
1041 data-live-update-field="<?php echo esc_attr( $live_update_field ); ?>"
1042 data-fallback-title="<?php echo esc_attr( $fallback_title ); ?>"
1043 <?php if ( ! empty( $live_update_options ) ) : ?>
1044 data-live-update-field-options="<?php echo esc_attr( wp_json_encode( $live_update_options ) ); ?>"
1045 <?php endif; ?>
1046 <?php if ( ! empty( $args['unique_field'] ) ) : ?>
1047 data-unique-field="<?php echo esc_attr( $args['unique_field'] ); ?>"
1048 <?php endif; ?>
1049 <?php echo $attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
1050
1051 <div class="<?php echo esc_attr( $args['id'] ); ?>-items wz-repeater-items">
1052 <?php
1053 if ( ! empty( $value ) ) {
1054 foreach ( array_values( $value ) as $index => $item ) {
1055 $this->render_repeater_item( $args, $index, $item );
1056 }
1057 }
1058 ?>
1059 </div>
1060 <button type="button" class="button add-item" data-target="<?php echo esc_attr( $args['id'] ); ?>">
1061 <?php echo esc_html( ! empty( $args['add_button_text'] ) ? $args['add_button_text'] : 'Add Item' ); ?>
1062 </button>
1063
1064 <template class="repeater-template" data-id="<?php echo esc_attr( $args['id'] ); ?>">
1065 <?php $this->render_repeater_item( $args, '{{INDEX}}' ); ?>
1066 </template>
1067 </div>
1068 <?php
1069 $html = ob_get_clean();
1070 $html .= $this->get_field_description( $args );
1071
1072 /**
1073 * This filter has been defined in class-settings-api.php
1074 */
1075 echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1076 }
1077
1078 /**
1079 * Render a single repeater item.
1080 *
1081 * @param array $args Repeater field arguments.
1082 * @param string|int $index Current item index.
1083 * @param array|null $item Item data if exists.
1084 * @return void
1085 */
1086 public function render_repeater_item( $args, $index, $item = null ) {
1087 if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
1088 return;
1089 }
1090
1091 $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
1092
1093 // Generate or retrieve unique row ID.
1094 $item_id = '';
1095 if ( is_array( $item ) && isset( $item['row_id'] ) ) {
1096 $item_id = $item['row_id'];
1097 } elseif ( '{{INDEX}}' !== $index ) {
1098 // For existing items without row_id, generate a persistent one.
1099 $item_id = 'row_' . md5( $args['id'] . '_' . $index );
1100 } else {
1101 // For new items, use a placeholder that will be replaced.
1102 $item_id = '{{ROW_ID}}';
1103 }
1104
1105 ?>
1106 <div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>">
1107 <input type="hidden" name="<?php echo esc_attr( $this->settings_key ); ?>[<?php echo esc_attr( $args['id'] ); ?>][<?php echo esc_attr( $index ); ?>][row_id]" value="<?php echo esc_attr( $item_id ); ?>" />
1108 <div class="repeater-item-header">
1109 <?php
1110 $display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
1111 $live_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array();
1112 $raw_live_value = ! empty( $item['fields'][ $display_field ] ) ? (string) $item['fields'][ $display_field ] : '';
1113 $display_value = '' !== $raw_live_value
1114 ? ( isset( $live_options[ $raw_live_value ] ) ? $live_options[ $raw_live_value ] : $raw_live_value )
1115 : $fallback_title;
1116 ?>
1117 <span class="repeater-title"><?php echo esc_html( $display_value ); ?></span>
1118 <span class="toggle-icon">▼</span>
1119 </div>
1120 <div class="repeater-item-content" style="display: none;">
1121 <?php
1122 foreach ( $args['fields'] as $field ) {
1123 $field_id = sanitize_key( $field['id'] );
1124
1125 $field_args = array_merge(
1126 (array) $field,
1127 array(
1128 'value' => isset( $item['fields'][ $field_id ] ) ? $item['fields'][ $field_id ] : ( isset( $field['default'] ) ? $field['default'] : '' ),
1129 '_repeater_id' => $args['id'],
1130 '_index' => $index,
1131 )
1132 );
1133 $field_args = Settings_API::parse_field_args( $field_args, $args['section'] );
1134
1135 if ( ! isset( $field['type'] ) || ! is_string( $field['type'] ) ) {
1136 continue;
1137 }
1138 ?>
1139 <?php $repeater_field_attributes = $this->get_field_attributes( $field_args ); ?>
1140 <div class="wz-repeater-field">
1141 <div class="wz-repeater-field-header">
1142 <label class="wz-repeater-field-label" for="<?php echo esc_attr( $repeater_field_attributes['field_id'] ); ?>">
1143 <?php echo esc_html( $field['name'] ); ?>
1144 <?php if ( ! empty( $field['required'] ) ) : ?>
1145 <span class="required" title="<?php echo esc_attr( $this->translation_strings['required_label'] ); ?>">*</span>
1146 <?php endif; ?>
1147 </label>
1148 </div>
1149
1150 <div class="wz-repeater-field-input">
1151 <?php
1152 $callback = 'callback_' . $field['type'];
1153
1154 if ( method_exists( $this, $callback ) ) {
1155 $this->$callback( $field_args );
1156 } else {
1157 do_action( "{$this->prefix}_repeater_field_{$field['type']}", $field_args, $index ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1158 }
1159 ?>
1160 </div>
1161 </div>
1162 <?php } ?>
1163 </div>
1164
1165 <div class="repeater-item-footer">
1166 <div class="repeater-item-actions">
1167 <button type="button" class="button button-secondary move-up">
1168 <span class="dashicons dashicons-arrow-up-alt2"></span>
1169 </button>
1170 <button type="button" class="button button-secondary move-down">
1171 <span class="dashicons dashicons-arrow-down-alt2"></span>
1172 </button>
1173 <button type="button" class="button button-secondary remove-item">
1174 <span class="dashicons dashicons-trash"></span>
1175 </button>
1176 </div>
1177 </div>
1178 </div>
1179
1180 <?php
1181 }
1182
1183
1184
1185 /**
1186 * Display sensitive fields.
1187 *
1188 * @since 1.0.0
1189 *
1190 * @param array $args Array of arguments.
1191 */
1192 public function callback_sensitive( $args ) {
1193 $encrypted_key = $this->get_field_value( $args );
1194 $decrypted_key = Settings_API::decrypt_api_key( $encrypted_key );
1195
1196 $args['value'] = $decrypted_key ? str_repeat( '*', strlen( $decrypted_key ) - 4 ) . substr( $decrypted_key, -4 ) : '';
1197
1198 $this->callback_text( $args );
1199 }
1200 }
1201