PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.2
WebberZone Top 10 — Popular Posts v4.5.2
4.5.2 4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 All 118 releases
← All changes | includes/admin/settings/class-settings-api.php +160 -57 4.3.3 → 4.5.2 View file →
@@ -17,9 +17,10 @@
17 17
18 18 /**
19 19 * Settings API wrapper class
20 20 *
21 - * @version 2.9.0
21 + * @version 3.0.0
22 + * @since 4.0.0
22 23 */
23 24 class Settings_API {
24 25
25 26 /**
@@ -26,9 +27,9 @@
26 27 * Current version number
27 28 *
28 29 * @var string
29 30 */
30 - public const VERSION = '2.9.0';
31 + public const VERSION = '3.0.0';
31 32
32 33 /**
33 34 * Settings Key.
34 35 *
@@ -73,8 +74,16 @@
73 74 */
74 75 protected $default_tab;
75 76
76 77 /**
78 + * Version used for cache-busting enqueued assets. Pass the plugin's own
79 + * version via props so every plugin release refreshes browser caches.
80 + *
81 + * @var string
82 + */
83 + protected $version = self::VERSION;
84 +
85 + /**
77 86 * Settings page.
78 87 *
79 88 * @var string Settings page.
80 89 */
@@ -124,10 +133,8 @@
124 133
125 134 /**
126 135 * Settings form.
127 136 *
128 - * @since 2.0.0
129 - *
130 137 * @var object Settings form.
131 138 */
132 139 public $settings_form;
133 140
@@ -217,8 +224,9 @@
217 224 'default_tab' => 'general',
218 225 'admin_footer_text' => '',
219 226 'help_sidebar' => '',
220 227 'help_tabs' => array(),
228 + 'version' => self::VERSION,
221 229 );
222 230
223 231 $args = wp_parse_args( $args, $defaults );
224 232
@@ -240,9 +248,8 @@
240 248 * @type string $success_message Success message.
241 249 * @type string $save_changes Save changes button label.
242 250 * @type string $reset_settings Reset settings button label.
243 251 * @type string $reset_button_confirm Reset button confirmation message.
244 - * @type string $checkbox_modified Checkbox modified label.
245 252 * }
246 253 *
247 254 * @return void
248 255 */
@@ -255,14 +262,22 @@
255 262 'success_message' => 'Settings updated.',
256 263 'save_changes' => 'Save Changes',
257 264 'reset_settings' => 'Reset all settings',
258 265 'reset_button_confirm' => 'Do you really want to reset all these settings to their default values?',
259 - 'checkbox_modified' => 'Modified from default setting',
260 266 'button_label' => 'Choose File',
261 267 'previous_saved' => 'Previously saved',
262 268 'repeater_new_item' => 'New Item',
269 + 'repeater_move_up' => 'Move item up',
270 + 'repeater_move_down' => 'Move item down',
271 + 'repeater_remove_item' => 'Remove item',
263 272 'required_label' => 'Required',
264 273 'tom_select_no_results' => 'No results found for "%s"',
274 + 'search_placeholder' => 'Search settings',
275 + 'search_no_results' => 'No settings found. Try a different search term.',
276 + 'search_clear' => 'Clear search',
277 + 'search_results_single' => '%d setting found.',
278 + 'search_results_plural' => '%d settings found.',
279 + 'search_matches_label' => 'matching settings',
265 280 );
266 281
267 282 $strings = wp_parse_args( $strings, $defaults );
268 283
@@ -483,9 +498,9 @@
483 498 wp_register_script(
484 499 'wz-' . $this->prefix . '-admin',
485 500 plugins_url( 'js/settings-admin-scripts' . $minimize . '.js', __FILE__ ),
486 501 array( 'jquery', 'wp-color-picker', 'jquery-ui-tabs' ),
487 - self::VERSION,
502 + $this->version,
488 503 true
489 504 );
490 505 wp_register_script(
491 506 'wz-' . $this->prefix . '-codemirror',
@@ -490,9 +505,9 @@
490 505 wp_register_script(
491 506 'wz-' . $this->prefix . '-codemirror',
492 507 plugins_url( 'js/apply-cm' . $minimize . '.js', __FILE__ ),
493 508 array( 'jquery', 'underscore', 'code-editor' ),
494 - self::VERSION,
509 + $this->version,
495 510 true
496 511 );
497 512 wp_register_script(
498 513 'wz-' . $this->prefix . '-media-selector',
@@ -497,9 +512,9 @@
497 512 wp_register_script(
498 513 'wz-' . $this->prefix . '-media-selector',
499 514 plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ),
500 515 array( 'jquery', 'media-editor', 'media-views' ),
501 - self::VERSION,
516 + $this->version,
502 517 true
503 518 );
504 519 wp_register_style(
505 520 'wz-' . $this->prefix . '-admin',
@@ -504,9 +519,9 @@
504 519 wp_register_style(
505 520 'wz-' . $this->prefix . '-admin',
506 521 plugins_url( 'css/admin-style' . $minimize . '.css', __FILE__ ),
507 522 array( 'wp-color-picker' ),
508 - self::VERSION
523 + $this->version
509 524 );
510 525
511 526 // Tom Select scripts and styles.
512 527 wp_register_style(
@@ -512,15 +527,15 @@
512 527 wp_register_style(
513 528 'wz-' . $this->prefix . '-tom-select',
514 529 plugins_url( 'css/tom-select.min.css', __FILE__ ),
515 530 array(),
516 - self::VERSION
531 + $this->version
517 532 );
518 533 wp_register_script(
519 534 'wz-' . $this->prefix . '-tom-select',
520 535 plugins_url( 'js/tom-select.complete.min.js', __FILE__ ),
521 536 array( 'jquery' ),
522 - self::VERSION,
537 + $this->version,
523 538 true
524 539 );
525 540 wp_register_script(
526 541 'wz-' . $this->prefix . '-tom-select-init',
@@ -525,17 +540,23 @@
525 540 wp_register_script(
526 541 'wz-' . $this->prefix . '-tom-select-init',
527 542 plugin_dir_url( __FILE__ ) . 'js/tom-select-init' . $minimize . '.js',
528 543 array( 'jquery', 'wz-' . $this->prefix . '-tom-select' ),
529 - self::VERSION,
544 + $this->version,
530 545 true
531 546 );
532 547 wp_localize_script(
533 548 "wz-{$this->prefix}-admin",
534 - 'WZSettingsAdmin',
549 + 'WebberSettingsAdmin',
535 550 array(
536 551 'prefix' => $this->prefix,
537 552 'settings_key' => $this->settings_key,
553 + 'strings' => array(
554 + 'search_no_results' => esc_html( $this->translation_strings['search_no_results'] ?? 'No settings found. Try a different search term.' ),
555 + 'search_results_single' => esc_html( $this->translation_strings['search_results_single'] ?? '%d setting found.' ),
556 + 'search_results_plural' => esc_html( $this->translation_strings['search_results_plural'] ?? '%d settings found.' ),
557 + 'search_matches_label' => esc_html( $this->translation_strings['search_matches_label'] ?? 'matching settings' ),
558 + ),
538 559 )
539 560 );
540 561
541 562 if ( $hook === $this->settings_page ) {
@@ -587,9 +608,9 @@
587 608
588 609 // Localize Tom Select settings.
589 610 wp_localize_script(
590 611 "wz-{$prefix}-tom-select-init",
591 - 'WZTomSelectSettings',
612 + 'WebberTomSelectSettings',
592 613 $args
593 614 );
594 615 wp_enqueue_script( "wz-{$prefix}-tom-select-init" );
595 616
@@ -636,12 +657,26 @@
636 657 $id = $args['id'];
637 658 $name = $args['name'];
638 659 $type = isset( $args['type'] ) ? $args['type'] : 'text';
639 660 $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' );
661 + $title = $name;
640 662
663 + if ( ! in_array( $type, array( 'header', 'repeater' ), true ) ) {
664 + $title = sprintf(
665 + '<label for="%1$s">%2$s</label>',
666 + esc_attr( $this->settings_form->get_field_id( $args ) ),
667 + wp_kses_post( $name )
668 + );
669 + }
670 +
671 + // Tag header rows so the settings search can group fields under them.
672 + if ( 'header' === $type ) {
673 + $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row', " \t\n\r\0\x0B" );
674 + }
675 +
641 676 add_settings_field(
642 677 "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array.
643 - $name, // Label of the setting.
678 + $title, // Label of the setting.
644 679 $callback, // Function to handle the setting.
645 680 "{$settings_key}_{$section}", // Page to display the setting. In our case it is the section as defined above.
646 681 "{$settings_key}_{$section}", // Name of the section.
647 682 $args
@@ -653,10 +688,13 @@
653 688 register_setting(
654 689 $settings_key,
655 690 $settings_key,
656 691 array(
692 + 'type' => 'object',
693 + 'default' => $this->settings_defaults(),
657 694 'sanitize_callback' => array( $this, 'settings_sanitize' ),
658 - 'show_in_rest' => true,
695 + // The value is an open-ended map with no REST schema, and settings_sanitize() expects a form submission.
696 + 'show_in_rest' => false,
659 697 )
660 698 );
661 699 }
662 700
@@ -791,28 +829,23 @@
791 829 if ( $setting['id'] !== $key ) {
792 830 continue;
793 831 }
794 832
795 - // Return the callback name.
796 - $sanitize_callback = false;
797 -
798 833 if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) {
799 - $sanitize_callback = $setting['sanitize_callback'];
800 - return $sanitize_callback;
834 + return $setting['sanitize_callback'];
801 835 }
802 836
803 - if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) {
804 - // For repeater fields, create a closure to pass the field configuration.
805 - if ( 'repeater' === $setting['type'] ) {
806 - return function ( $value ) use ( $settings_sanitize, $setting ) {
807 - return $settings_sanitize->sanitize_repeater_field( $value, $setting );
808 - };
809 - }
810 - $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' );
811 - return $sanitize_callback;
837 + $method = 'sanitize_' . $setting['type'] . '_field';
838 +
839 + // Field types with no callback of their own must still not store raw input.
840 + if ( ! is_callable( array( $settings_sanitize, $method ) ) ) {
841 + $method = 'sanitize_missing';
812 842 }
813 843
814 - return $sanitize_callback;
844 + // Every callback receives the field configuration so choice fields can validate against their own options.
845 + return function ( $value ) use ( $settings_sanitize, $method, $setting ) {
846 + return $settings_sanitize->$method( $value, $setting );
847 + };
815 848 }
816 849 }
817 850
818 851 return false;
@@ -818,21 +851,40 @@
818 851 return false;
819 852 }
820 853
821 854 /**
855 + * Get the settings keys that are rendered locked (disabled or pro-gated).
856 + *
857 + * @return array Map of settings key => true for each locked setting.
858 + */
859 + public function get_locked_settings() {
860 + $locked = array();
861 +
862 + foreach ( $this->registered_settings as $settings ) {
863 + foreach ( $settings as $setting ) {
864 + if ( isset( $setting['id'] ) && ( ! empty( $setting['disabled'] ) || ! empty( $setting['pro'] ) ) ) {
865 + $locked[ $setting['id'] ] = true;
866 + }
867 + }
868 + }
869 +
870 + return $locked;
871 + }
872 +
873 + /**
822 874 * Sanitize the form data being submitted.
823 875 *
824 - * @param array $input Input unclean array.
876 + * @param mixed $input Unsanitized input. An array for form submissions, but REST and WP-CLI may pass anything.
825 877 * @return array Sanitized array
826 878 */
827 879 public function settings_sanitize( $input ) {
828 - // This should be set if a form is submitted, so let's save it in the $referrer variable.
829 - if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
830 - return $input;
880 + // Set when a classic form is submitted; used only to pick the active tab below.
881 + $referrer = array();
882 +
883 + if ( ! empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
884 + parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
831 885 }
832 886
833 - parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
834 -
835 887 // Check if we need to set to defaults.
836 888 $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
837 889
838 890 if ( $reset ) {
@@ -847,13 +899,14 @@
847 899 // Get the various settings we've registered.
848 900 $settings = get_option( $this->settings_key );
849 901 $settings = is_array( $settings ) ? $settings : array();
850 902 $settings_types = $this->get_registered_settings_types();
903 + $locked = $this->get_locked_settings();
851 904
852 905 // Get the tab. This is also our settings' section.
853 906 $tab = $referrer['tab'] ?? $this->default_tab;
854 907
855 - $input = $input ? $input : array();
908 + $input = is_array( $input ) ? $input : array();
856 909
857 910 /**
858 911 * Filter the settings for the tab. e.g. prefix_settings_general_sanitize.
859 912 *
@@ -860,10 +913,10 @@
860 913 * @param array $input Input unclean array
861 914 */
862 915 $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
863 916
864 - // Create an output array by merging the existing settings with the ones submitted.
865 - $output = array_merge( $settings, $input );
917 + // Start from what is stored. Submitted values are merged back in below, once sanitized.
918 + $output = $settings;
866 919
867 920 // Loop through each setting being saved and pass it through a sanitization filter.
868 921 foreach ( $settings_types as $key => $type ) {
869 922 /**
@@ -881,13 +934,9 @@
881 934 $sanitize_callback = $this->get_sanitize_callback( $key );
882 935
883 936 // If callback is set, call it.
884 937 if ( $sanitize_callback ) {
885 - if ( 'sensitive' === $type ) {
886 - $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key );
887 - } else {
888 - $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
889 - }
938 + $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
890 939 continue;
891 940 }
892 941 }
893 942
@@ -892,15 +941,25 @@
892 941 }
893 942
894 943 // Delete any key that is not present when we submit the input array.
895 944 if ( ! isset( $input[ $key ] ) ) {
896 - unset( $output[ $key ] );
945 + // Disabled fields are never submitted, so a missing key must not delete them.
946 + if ( ! isset( $locked[ $key ] ) ) {
947 + unset( $output[ $key ] );
948 + }
897 949 }
950 + }
898 951
899 - // Delete any settings that are no longer part of our registered settings.
900 - if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) {
901 - unset( $output[ $key ] );
902 - }
952 + // Keys added by the tab filter are not registered settings, but must not be stored raw either.
953 + $settings_sanitize = new Settings_Sanitize(
954 + array(
955 + 'settings_key' => $this->settings_key,
956 + 'prefix' => $this->prefix,
957 + )
958 + );
959 +
960 + foreach ( array_diff_key( $input, $settings_types ) as $key => $value ) {
961 + $output[ sanitize_text_field( (string) $key ) ] = $settings_sanitize->sanitize_missing( $value );
903 962 }
904 963
905 964 add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' );
906 965
@@ -932,10 +991,10 @@
932 991 }
933 992 ?>
934 993
935 994 <div id="poststuff">
936 - <div id="post-body" class="metabox-holder columns-2">
937 - <div id="post-body-content">
995 + <div id="post-body" class="metabox-holder columns-2 wz-settings-post-body">
996 + <div id="post-body-content" class="wz-vertical-tabs">
938 997
939 998 <?php $this->show_navigation(); ?>
940 999 <?php $this->show_form(); ?>
941 1000
@@ -969,10 +1028,8 @@
969 1028 */
970 1029 public function show_navigation() {
971 1030 $active_tab = isset( $_GET['tab'] ) && array_key_exists( sanitize_key( wp_unslash( $_GET['tab'] ) ), $this->settings_sections ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : $this->default_tab; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
972 1031
973 - $html = '<ul class="nav-tab-wrapper" style="padding:0">';
974 -
975 1032 $count = count( $this->settings_sections );
976 1033
977 1034 // Don't show the navigation if only one section exists.
978 1035 if ( 1 === $count ) {
@@ -978,14 +1035,32 @@
978 1035 if ( 1 === $count ) {
979 1036 return;
980 1037 }
981 1038
1039 + $html = '<ul class="nav-tab-wrapper">';
1040 +
1041 + // Settings search box. Rendered via wp_kses() with an extended allowed list below as
1042 + // wp_kses_post() strips input and button tags.
1043 + $search_box = sprintf(
1044 + '<li class="wz-settings-search-wrap">' .
1045 + '<label class="screen-reader-text" for="%1$s">%2$s</label>' .
1046 + '<span class="wz-settings-search-box">' .
1047 + '<input type="search" id="%1$s" class="wz-settings-search" placeholder="%2$s" autocomplete="off" />' .
1048 + '<button type="button" class="wz-settings-search-clear" aria-label="%3$s" hidden><span aria-hidden="true">&times;</span></button>' .
1049 + '</span>' .
1050 + '<span class="wz-settings-search-status screen-reader-text" role="status" aria-live="polite"></span>' .
1051 + '</li>',
1052 + esc_attr( "{$this->prefix}-settings-search" ),
1053 + esc_attr( $this->translation_strings['search_placeholder'] ?? 'Search settings' ),
1054 + esc_attr( $this->translation_strings['search_clear'] ?? 'Clear search' )
1055 + );
1056 +
982 1057 foreach ( $this->settings_sections as $tab_id => $tab_name ) {
983 1058
984 1059 $active = $active_tab === $tab_id ? ' ' : '';
985 1060
986 1061 $html .= sprintf(
987 - '<li style="padding:0; border:0; margin:0;"><a href="#%s" title="%s" class="nav-tab %s">%s</a></li>',
1062 + '<li><a href="#%s" title="%s" class="nav-tab %s">%s</a></li>',
988 1063 esc_attr( $tab_id ),
989 1064 esc_attr( $tab_name ),
990 1065 sanitize_html_class( $active ),
991 1066 esc_html( $tab_name )
@@ -994,9 +1069,35 @@
994 1069 }
995 1070
996 1071 $html .= '</ul>';
997 1072
998 - echo wp_kses_post( $html );
1073 + $allowed_html = wp_kses_allowed_html( 'post' );
1074 + $allowed_html['input'] = array(
1075 + 'type' => true,
1076 + 'id' => true,
1077 + 'class' => true,
1078 + 'placeholder' => true,
1079 + 'autocomplete' => true,
1080 + );
1081 + $allowed_html['button'] = array(
1082 + 'type' => true,
1083 + 'class' => true,
1084 + 'aria-label' => true,
1085 + 'hidden' => true,
1086 + );
1087 + $allowed_html['span'] = array_merge(
1088 + (array) ( $allowed_html['span'] ?? array() ),
1089 + array(
1090 + 'class' => true,
1091 + 'role' => true,
1092 + 'aria-live' => true,
1093 + 'aria-hidden' => true,
1094 + )
1095 + );
1096 +
1097 + $html = str_replace( '<ul class="nav-tab-wrapper">', '<ul class="nav-tab-wrapper">' . $search_box, $html );
1098 +
1099 + echo wp_kses( $html, $allowed_html );
999 1100 }
1000 1101
1001 1102 /**
1002 1103 * Show the section settings forms
@@ -1012,8 +1113,9 @@
1012 1113
1013 1114 <?php foreach ( $this->settings_sections as $tab_id => $tab_name ) : ?>
1014 1115
1015 1116 <div id="<?php echo esc_attr( $tab_id ); ?>">
1117 + <h2 class="wz-section-title" tabindex="-1"><?php echo esc_html( $tab_name ); ?></h2>
1016 1118 <table class="form-table">
1017 1119 <?php
1018 1120 do_settings_fields( $this->prefix . '_settings_' . $tab_id, $this->prefix . '_settings_' . $tab_id );
1019 1121 ?>
@@ -1053,8 +1155,9 @@
1053 1155 */
1054 1156 do_action( $this->prefix . '_settings_form_buttons', $tab_id, $tab_name, $this->settings_sections ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1055 1157 ?>
1056 1158 </p>
1159 + <p class="wz-modified-legend"><span class="wz-modified-dot"></span><?php echo esc_html( $this->translation_strings['modified_legend'] ?? 'Setting modified from its default value' ); ?></p>
1057 1160 </div><!-- /#tab_id-->
1058 1161
1059 1162 <?php endforeach; ?>
1060 1163