PluginProbe
WebberZone Top 10 — Popular Posts / 4.5.1
WebberZone Top 10 — Popular Posts v4.5.1
4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 All 117 releases
← All changes | includes/admin/settings/class-settings-api.php +147 -56 4.3.2 → 4.5.1 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,19 @@
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',
263 269 'required_label' => 'Required',
264 270 'tom_select_no_results' => 'No results found for "%s"',
271 + 'search_placeholder' => 'Search settings',
272 + 'search_no_results' => 'No settings found. Try a different search term.',
273 + 'search_clear' => 'Clear search',
274 + 'search_results_single' => '%d setting found.',
275 + 'search_results_plural' => '%d settings found.',
276 + 'search_matches_label' => 'matching settings',
265 277 );
266 278
267 279 $strings = wp_parse_args( $strings, $defaults );
268 280
@@ -483,9 +495,9 @@
483 495 wp_register_script(
484 496 'wz-' . $this->prefix . '-admin',
485 497 plugins_url( 'js/settings-admin-scripts' . $minimize . '.js', __FILE__ ),
486 498 array( 'jquery', 'wp-color-picker', 'jquery-ui-tabs' ),
487 - self::VERSION,
499 + $this->version,
488 500 true
489 501 );
490 502 wp_register_script(
491 503 'wz-' . $this->prefix . '-codemirror',
@@ -490,9 +502,9 @@
490 502 wp_register_script(
491 503 'wz-' . $this->prefix . '-codemirror',
492 504 plugins_url( 'js/apply-cm' . $minimize . '.js', __FILE__ ),
493 505 array( 'jquery', 'underscore', 'code-editor' ),
494 - self::VERSION,
506 + $this->version,
495 507 true
496 508 );
497 509 wp_register_script(
498 510 'wz-' . $this->prefix . '-media-selector',
@@ -497,9 +509,9 @@
497 509 wp_register_script(
498 510 'wz-' . $this->prefix . '-media-selector',
499 511 plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ),
500 512 array( 'jquery', 'media-editor', 'media-views' ),
501 - self::VERSION,
513 + $this->version,
502 514 true
503 515 );
504 516 wp_register_style(
505 517 'wz-' . $this->prefix . '-admin',
@@ -504,9 +516,9 @@
504 516 wp_register_style(
505 517 'wz-' . $this->prefix . '-admin',
506 518 plugins_url( 'css/admin-style' . $minimize . '.css', __FILE__ ),
507 519 array( 'wp-color-picker' ),
508 - self::VERSION
520 + $this->version
509 521 );
510 522
511 523 // Tom Select scripts and styles.
512 524 wp_register_style(
@@ -512,15 +524,15 @@
512 524 wp_register_style(
513 525 'wz-' . $this->prefix . '-tom-select',
514 526 plugins_url( 'css/tom-select.min.css', __FILE__ ),
515 527 array(),
516 - self::VERSION
528 + $this->version
517 529 );
518 530 wp_register_script(
519 531 'wz-' . $this->prefix . '-tom-select',
520 532 plugins_url( 'js/tom-select.complete.min.js', __FILE__ ),
521 533 array( 'jquery' ),
522 - self::VERSION,
534 + $this->version,
523 535 true
524 536 );
525 537 wp_register_script(
526 538 'wz-' . $this->prefix . '-tom-select-init',
@@ -525,17 +537,23 @@
525 537 wp_register_script(
526 538 'wz-' . $this->prefix . '-tom-select-init',
527 539 plugin_dir_url( __FILE__ ) . 'js/tom-select-init' . $minimize . '.js',
528 540 array( 'jquery', 'wz-' . $this->prefix . '-tom-select' ),
529 - self::VERSION,
541 + $this->version,
530 542 true
531 543 );
532 544 wp_localize_script(
533 545 "wz-{$this->prefix}-admin",
534 - 'WZSettingsAdmin',
546 + 'WebberSettingsAdmin',
535 547 array(
536 548 'prefix' => $this->prefix,
537 549 'settings_key' => $this->settings_key,
550 + 'strings' => array(
551 + 'search_no_results' => esc_html( $this->translation_strings['search_no_results'] ?? 'No settings found. Try a different search term.' ),
552 + 'search_results_single' => esc_html( $this->translation_strings['search_results_single'] ?? '%d setting found.' ),
553 + 'search_results_plural' => esc_html( $this->translation_strings['search_results_plural'] ?? '%d settings found.' ),
554 + 'search_matches_label' => esc_html( $this->translation_strings['search_matches_label'] ?? 'matching settings' ),
555 + ),
538 556 )
539 557 );
540 558
541 559 if ( $hook === $this->settings_page ) {
@@ -587,9 +605,9 @@
587 605
588 606 // Localize Tom Select settings.
589 607 wp_localize_script(
590 608 "wz-{$prefix}-tom-select-init",
591 - 'WZTomSelectSettings',
609 + 'WebberTomSelectSettings',
592 610 $args
593 611 );
594 612 wp_enqueue_script( "wz-{$prefix}-tom-select-init" );
595 613
@@ -637,8 +655,13 @@
637 655 $name = $args['name'];
638 656 $type = isset( $args['type'] ) ? $args['type'] : 'text';
639 657 $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' );
640 658
659 + // Tag header rows so the settings search can group fields under them.
660 + if ( 'header' === $type ) {
661 + $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row', " \t\n\r\0\x0B" );
662 + }
663 +
641 664 add_settings_field(
642 665 "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array.
643 666 $name, // Label of the setting.
644 667 $callback, // Function to handle the setting.
@@ -653,10 +676,13 @@
653 676 register_setting(
654 677 $settings_key,
655 678 $settings_key,
656 679 array(
680 + 'type' => 'object',
681 + 'default' => $this->settings_defaults(),
657 682 'sanitize_callback' => array( $this, 'settings_sanitize' ),
658 - 'show_in_rest' => true,
683 + // The value is an open-ended map with no REST schema, and settings_sanitize() expects a form submission.
684 + 'show_in_rest' => false,
659 685 )
660 686 );
661 687 }
662 688
@@ -791,28 +817,23 @@
791 817 if ( $setting['id'] !== $key ) {
792 818 continue;
793 819 }
794 820
795 - // Return the callback name.
796 - $sanitize_callback = false;
797 -
798 821 if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) {
799 - $sanitize_callback = $setting['sanitize_callback'];
800 - return $sanitize_callback;
822 + return $setting['sanitize_callback'];
801 823 }
802 824
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;
825 + $method = 'sanitize_' . $setting['type'] . '_field';
826 +
827 + // Field types with no callback of their own must still not store raw input.
828 + if ( ! is_callable( array( $settings_sanitize, $method ) ) ) {
829 + $method = 'sanitize_missing';
812 830 }
813 831
814 - return $sanitize_callback;
832 + // Every callback receives the field configuration so choice fields can validate against their own options.
833 + return function ( $value ) use ( $settings_sanitize, $method, $setting ) {
834 + return $settings_sanitize->$method( $value, $setting );
835 + };
815 836 }
816 837 }
817 838
818 839 return false;
@@ -818,21 +839,40 @@
818 839 return false;
819 840 }
820 841
821 842 /**
843 + * Get the settings keys that are rendered locked (disabled or pro-gated).
844 + *
845 + * @return array Map of settings key => true for each locked setting.
846 + */
847 + public function get_locked_settings() {
848 + $locked = array();
849 +
850 + foreach ( $this->registered_settings as $settings ) {
851 + foreach ( $settings as $setting ) {
852 + if ( isset( $setting['id'] ) && ( ! empty( $setting['disabled'] ) || ! empty( $setting['pro'] ) ) ) {
853 + $locked[ $setting['id'] ] = true;
854 + }
855 + }
856 + }
857 +
858 + return $locked;
859 + }
860 +
861 + /**
822 862 * Sanitize the form data being submitted.
823 863 *
824 - * @param array $input Input unclean array.
864 + * @param mixed $input Unsanitized input. An array for form submissions, but REST and WP-CLI may pass anything.
825 865 * @return array Sanitized array
826 866 */
827 867 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;
868 + // Set when a classic form is submitted; used only to pick the active tab below.
869 + $referrer = array();
870 +
871 + if ( ! empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
872 + parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
831 873 }
832 874
833 - parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
834 -
835 875 // Check if we need to set to defaults.
836 876 $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
837 877
838 878 if ( $reset ) {
@@ -847,13 +887,14 @@
847 887 // Get the various settings we've registered.
848 888 $settings = get_option( $this->settings_key );
849 889 $settings = is_array( $settings ) ? $settings : array();
850 890 $settings_types = $this->get_registered_settings_types();
891 + $locked = $this->get_locked_settings();
851 892
852 893 // Get the tab. This is also our settings' section.
853 894 $tab = $referrer['tab'] ?? $this->default_tab;
854 895
855 - $input = $input ? $input : array();
896 + $input = is_array( $input ) ? $input : array();
856 897
857 898 /**
858 899 * Filter the settings for the tab. e.g. prefix_settings_general_sanitize.
859 900 *
@@ -860,10 +901,10 @@
860 901 * @param array $input Input unclean array
861 902 */
862 903 $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
863 904
864 - // Create an output array by merging the existing settings with the ones submitted.
865 - $output = array_merge( $settings, $input );
905 + // Start from what is stored. Submitted values are merged back in below, once sanitized.
906 + $output = $settings;
866 907
867 908 // Loop through each setting being saved and pass it through a sanitization filter.
868 909 foreach ( $settings_types as $key => $type ) {
869 910 /**
@@ -881,13 +922,9 @@
881 922 $sanitize_callback = $this->get_sanitize_callback( $key );
882 923
883 924 // If callback is set, call it.
884 925 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 - }
926 + $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
890 927 continue;
891 928 }
892 929 }
893 930
@@ -892,15 +929,25 @@
892 929 }
893 930
894 931 // Delete any key that is not present when we submit the input array.
895 932 if ( ! isset( $input[ $key ] ) ) {
896 - unset( $output[ $key ] );
933 + // Disabled fields are never submitted, so a missing key must not delete them.
934 + if ( ! isset( $locked[ $key ] ) ) {
935 + unset( $output[ $key ] );
936 + }
897 937 }
938 + }
898 939
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 - }
940 + // Keys added by the tab filter are not registered settings, but must not be stored raw either.
941 + $settings_sanitize = new Settings_Sanitize(
942 + array(
943 + 'settings_key' => $this->settings_key,
944 + 'prefix' => $this->prefix,
945 + )
946 + );
947 +
948 + foreach ( array_diff_key( $input, $settings_types ) as $key => $value ) {
949 + $output[ sanitize_text_field( (string) $key ) ] = $settings_sanitize->sanitize_missing( $value );
903 950 }
904 951
905 952 add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' );
906 953
@@ -932,10 +979,10 @@
932 979 }
933 980 ?>
934 981
935 982 <div id="poststuff">
936 - <div id="post-body" class="metabox-holder columns-2">
937 - <div id="post-body-content">
983 + <div id="post-body" class="metabox-holder columns-2 wz-settings-post-body">
984 + <div id="post-body-content" class="wz-vertical-tabs">
938 985
939 986 <?php $this->show_navigation(); ?>
940 987 <?php $this->show_form(); ?>
941 988
@@ -969,10 +1016,8 @@
969 1016 */
970 1017 public function show_navigation() {
971 1018 $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 1019
973 - $html = '<ul class="nav-tab-wrapper" style="padding:0">';
974 -
975 1020 $count = count( $this->settings_sections );
976 1021
977 1022 // Don't show the navigation if only one section exists.
978 1023 if ( 1 === $count ) {
@@ -978,14 +1023,32 @@
978 1023 if ( 1 === $count ) {
979 1024 return;
980 1025 }
981 1026
1027 + $html = '<ul class="nav-tab-wrapper">';
1028 +
1029 + // Settings search box. Rendered via wp_kses() with an extended allowed list below as
1030 + // wp_kses_post() strips input and button tags.
1031 + $search_box = sprintf(
1032 + '<li class="wz-settings-search-wrap">' .
1033 + '<label class="screen-reader-text" for="%1$s">%2$s</label>' .
1034 + '<span class="wz-settings-search-box">' .
1035 + '<input type="search" id="%1$s" class="wz-settings-search" placeholder="%2$s" autocomplete="off" />' .
1036 + '<button type="button" class="wz-settings-search-clear" aria-label="%3$s" hidden><span aria-hidden="true">&times;</span></button>' .
1037 + '</span>' .
1038 + '<span class="wz-settings-search-status screen-reader-text" role="status" aria-live="polite"></span>' .
1039 + '</li>',
1040 + esc_attr( "{$this->prefix}-settings-search" ),
1041 + esc_attr( $this->translation_strings['search_placeholder'] ?? 'Search settings' ),
1042 + esc_attr( $this->translation_strings['search_clear'] ?? 'Clear search' )
1043 + );
1044 +
982 1045 foreach ( $this->settings_sections as $tab_id => $tab_name ) {
983 1046
984 1047 $active = $active_tab === $tab_id ? ' ' : '';
985 1048
986 1049 $html .= sprintf(
987 - '<li style="padding:0; border:0; margin:0;"><a href="#%s" title="%s" class="nav-tab %s">%s</a></li>',
1050 + '<li><a href="#%s" title="%s" class="nav-tab %s">%s</a></li>',
988 1051 esc_attr( $tab_id ),
989 1052 esc_attr( $tab_name ),
990 1053 sanitize_html_class( $active ),
991 1054 esc_html( $tab_name )
@@ -994,9 +1057,35 @@
994 1057 }
995 1058
996 1059 $html .= '</ul>';
997 1060
998 - echo wp_kses_post( $html );
1061 + $allowed_html = wp_kses_allowed_html( 'post' );
1062 + $allowed_html['input'] = array(
1063 + 'type' => true,
1064 + 'id' => true,
1065 + 'class' => true,
1066 + 'placeholder' => true,
1067 + 'autocomplete' => true,
1068 + );
1069 + $allowed_html['button'] = array(
1070 + 'type' => true,
1071 + 'class' => true,
1072 + 'aria-label' => true,
1073 + 'hidden' => true,
1074 + );
1075 + $allowed_html['span'] = array_merge(
1076 + (array) ( $allowed_html['span'] ?? array() ),
1077 + array(
1078 + 'class' => true,
1079 + 'role' => true,
1080 + 'aria-live' => true,
1081 + 'aria-hidden' => true,
1082 + )
1083 + );
1084 +
1085 + $html = str_replace( '<ul class="nav-tab-wrapper">', '<ul class="nav-tab-wrapper">' . $search_box, $html );
1086 +
1087 + echo wp_kses( $html, $allowed_html );
999 1088 }
1000 1089
1001 1090 /**
1002 1091 * Show the section settings forms
@@ -1012,8 +1101,9 @@
1012 1101
1013 1102 <?php foreach ( $this->settings_sections as $tab_id => $tab_name ) : ?>
1014 1103
1015 1104 <div id="<?php echo esc_attr( $tab_id ); ?>">
1105 + <h2 class="wz-section-title" tabindex="-1"><?php echo esc_html( $tab_name ); ?></h2>
1016 1106 <table class="form-table">
1017 1107 <?php
1018 1108 do_settings_fields( $this->prefix . '_settings_' . $tab_id, $this->prefix . '_settings_' . $tab_id );
1019 1109 ?>
@@ -1053,8 +1143,9 @@
1053 1143 */
1054 1144 do_action( $this->prefix . '_settings_form_buttons', $tab_id, $tab_name, $this->settings_sections ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1055 1145 ?>
1056 1146 </p>
1147 + <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 1148 </div><!-- /#tab_id-->
1058 1149
1059 1150 <?php endforeach; ?>
1060 1151