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

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

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