| @@ -17,9 +17,9 @@ | ||
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | 19 | * Settings API wrapper class |
| 20 | 20 | * |
| 21 | - * @version 2.9.0 | |
| 21 | + * @version 3.0.0 | |
| 22 | 22 | * @since 4.0.0 |
| 23 | 23 | */ |
| 24 | 24 | class Settings_API { |
| 25 | 25 | |
| @@ -27,9 +27,9 @@ | ||
| 27 | 27 | * Current version number |
| 28 | 28 | * |
| 29 | 29 | * @var string |
| 30 | 30 | */ |
| 31 | - public const VERSION = '2.11.0'; | |
| 31 | + public const VERSION = '3.0.0'; | |
| 32 | 32 | |
| 33 | 33 | /** |
| 34 | 34 | * Settings Key. |
| 35 | 35 | * |
| @@ -265,8 +265,11 @@ | ||
| 265 | 265 | 'reset_button_confirm' => 'Do you really want to reset all these settings to their default values?', |
| 266 | 266 | 'button_label' => 'Choose File', |
| 267 | 267 | 'previous_saved' => 'Previously saved', |
| 268 | 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', | |
| 269 | 272 | 'required_label' => 'Required', |
| 270 | 273 | 'tom_select_no_results' => 'No results found for "%s"', |
| 271 | 274 | 'search_placeholder' => 'Search settings', |
| 272 | 275 | 'search_no_results' => 'No settings found. Try a different search term.', |
| @@ -542,9 +545,9 @@ | ||
| 542 | 545 | true |
| 543 | 546 | ); |
| 544 | 547 | wp_localize_script( |
| 545 | 548 | "wz-{$this->prefix}-admin", |
| 546 | - 'WZSettingsAdmin', | |
| 549 | + 'WebberSettingsAdmin', | |
| 547 | 550 | array( |
| 548 | 551 | 'prefix' => $this->prefix, |
| 549 | 552 | 'settings_key' => $this->settings_key, |
| 550 | 553 | 'strings' => array( |
| @@ -605,9 +608,9 @@ | ||
| 605 | 608 | |
| 606 | 609 | // Localize Tom Select settings. |
| 607 | 610 | wp_localize_script( |
| 608 | 611 | "wz-{$prefix}-tom-select-init", |
| 609 | - 'WZTomSelectSettings', | |
| 612 | + 'WebberTomSelectSettings', | |
| 610 | 613 | $args |
| 611 | 614 | ); |
| 612 | 615 | wp_enqueue_script( "wz-{$prefix}-tom-select-init" ); |
| 613 | 616 | |
| @@ -654,17 +657,26 @@ | ||
| 654 | 657 | $id = $args['id']; |
| 655 | 658 | $name = $args['name']; |
| 656 | 659 | $type = isset( $args['type'] ) ? $args['type'] : 'text'; |
| 657 | 660 | $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' ); |
| 661 | + $title = $name; | |
| 658 | 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 | + | |
| 659 | 671 | // Tag header rows so the settings search can group fields under them. |
| 660 | 672 | if ( 'header' === $type ) { |
| 661 | - $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row' ); | |
| 673 | + $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row', " \t\n\r\0\x0B" ); | |
| 662 | 674 | } |
| 663 | 675 | |
| 664 | 676 | add_settings_field( |
| 665 | 677 | "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array. |
| 666 | - $name, // Label of the setting. | |
| 678 | + $title, // Label of the setting. | |
| 667 | 679 | $callback, // Function to handle the setting. |
| 668 | 680 | "{$settings_key}_{$section}", // Page to display the setting. In our case it is the section as defined above. |
| 669 | 681 | "{$settings_key}_{$section}", // Name of the section. |
| 670 | 682 | $args |
| @@ -676,10 +688,13 @@ | ||
| 676 | 688 | register_setting( |
| 677 | 689 | $settings_key, |
| 678 | 690 | $settings_key, |
| 679 | 691 | array( |
| 692 | + 'type' => 'object', | |
| 693 | + 'default' => $this->settings_defaults(), | |
| 680 | 694 | 'sanitize_callback' => array( $this, 'settings_sanitize' ), |
| 681 | - '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, | |
| 682 | 697 | ) |
| 683 | 698 | ); |
| 684 | 699 | } |
| 685 | 700 | |
| @@ -814,28 +829,23 @@ | ||
| 814 | 829 | if ( $setting['id'] !== $key ) { |
| 815 | 830 | continue; |
| 816 | 831 | } |
| 817 | 832 | |
| 818 | - // Return the callback name. | |
| 819 | - $sanitize_callback = false; | |
| 820 | - | |
| 821 | 833 | if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) { |
| 822 | - $sanitize_callback = $setting['sanitize_callback']; | |
| 823 | - return $sanitize_callback; | |
| 834 | + return $setting['sanitize_callback']; | |
| 824 | 835 | } |
| 825 | 836 | |
| 826 | - if ( is_callable( array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ) ) ) { | |
| 827 | - // For repeater fields, create a closure to pass the field configuration. | |
| 828 | - if ( 'repeater' === $setting['type'] ) { | |
| 829 | - return function ( $value ) use ( $settings_sanitize, $setting ) { | |
| 830 | - return $settings_sanitize->sanitize_repeater_field( $value, $setting ); | |
| 831 | - }; | |
| 832 | - } | |
| 833 | - $sanitize_callback = array( $settings_sanitize, 'sanitize_' . $setting['type'] . '_field' ); | |
| 834 | - 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'; | |
| 835 | 842 | } |
| 836 | 843 | |
| 837 | - 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 | + }; | |
| 838 | 848 | } |
| 839 | 849 | } |
| 840 | 850 | |
| 841 | 851 | return false; |
| @@ -841,21 +851,40 @@ | ||
| 841 | 851 | return false; |
| 842 | 852 | } |
| 843 | 853 | |
| 844 | 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 | + /** | |
| 845 | 874 | * Sanitize the form data being submitted. |
| 846 | 875 | * |
| 847 | - * @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. | |
| 848 | 877 | * @return array Sanitized array |
| 849 | 878 | */ |
| 850 | 879 | public function settings_sanitize( $input ) { |
| 851 | - // This should be set if a form is submitted, so let's save it in the $referrer variable. | |
| 852 | - if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 853 | - 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 | |
| 854 | 885 | } |
| 855 | 886 | |
| 856 | - parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 857 | - | |
| 858 | 887 | // Check if we need to set to defaults. |
| 859 | 888 | $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 860 | 889 | |
| 861 | 890 | if ( $reset ) { |
| @@ -870,13 +899,14 @@ | ||
| 870 | 899 | // Get the various settings we've registered. |
| 871 | 900 | $settings = get_option( $this->settings_key ); |
| 872 | 901 | $settings = is_array( $settings ) ? $settings : array(); |
| 873 | 902 | $settings_types = $this->get_registered_settings_types(); |
| 903 | + $locked = $this->get_locked_settings(); | |
| 874 | 904 | |
| 875 | 905 | // Get the tab. This is also our settings' section. |
| 876 | 906 | $tab = $referrer['tab'] ?? $this->default_tab; |
| 877 | 907 | |
| 878 | - $input = $input ? $input : array(); | |
| 908 | + $input = is_array( $input ) ? $input : array(); | |
| 879 | 909 | |
| 880 | 910 | /** |
| 881 | 911 | * Filter the settings for the tab. e.g. prefix_settings_general_sanitize. |
| 882 | 912 | * |
| @@ -883,10 +913,10 @@ | ||
| 883 | 913 | * @param array $input Input unclean array |
| 884 | 914 | */ |
| 885 | 915 | $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound |
| 886 | 916 | |
| 887 | - // Create an output array by merging the existing settings with the ones submitted. | |
| 888 | - $output = array_merge( $settings, $input ); | |
| 917 | + // Start from what is stored. Submitted values are merged back in below, once sanitized. | |
| 918 | + $output = $settings; | |
| 889 | 919 | |
| 890 | 920 | // Loop through each setting being saved and pass it through a sanitization filter. |
| 891 | 921 | foreach ( $settings_types as $key => $type ) { |
| 892 | 922 | /** |
| @@ -904,13 +934,9 @@ | ||
| 904 | 934 | $sanitize_callback = $this->get_sanitize_callback( $key ); |
| 905 | 935 | |
| 906 | 936 | // If callback is set, call it. |
| 907 | 937 | if ( $sanitize_callback ) { |
| 908 | - if ( 'sensitive' === $type ) { | |
| 909 | - $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key ); | |
| 910 | - } else { | |
| 911 | - $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] ); | |
| 912 | - } | |
| 938 | + $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] ); | |
| 913 | 939 | continue; |
| 914 | 940 | } |
| 915 | 941 | } |
| 916 | 942 | |
| @@ -915,15 +941,25 @@ | ||
| 915 | 941 | } |
| 916 | 942 | |
| 917 | 943 | // Delete any key that is not present when we submit the input array. |
| 918 | 944 | if ( ! isset( $input[ $key ] ) ) { |
| 919 | - 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 | + } | |
| 920 | 949 | } |
| 950 | + } | |
| 921 | 951 | |
| 922 | - // Delete any settings that are no longer part of our registered settings. | |
| 923 | - if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) { | |
| 924 | - unset( $output[ $key ] ); | |
| 925 | - } | |
| 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 ); | |
| 926 | 962 | } |
| 927 | 963 | |
| 928 | 964 | add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' ); |
| 929 | 965 | |
| @@ -955,9 +991,9 @@ | ||
| 955 | 991 | } |
| 956 | 992 | ?> |
| 957 | 993 | |
| 958 | 994 | <div id="poststuff"> |
| 959 | - <div id="post-body" class="metabox-holder columns-2"> | |
| 995 | + <div id="post-body" class="metabox-holder columns-2 wz-settings-post-body"> | |
| 960 | 996 | <div id="post-body-content" class="wz-vertical-tabs"> |
| 961 | 997 | |
| 962 | 998 | <?php $this->show_navigation(); ?> |
| 963 | 999 | <?php $this->show_form(); ?> |