| @@ -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 | * |
| @@ -542,9 +542,9 @@ | ||
| 542 | 542 | true |
| 543 | 543 | ); |
| 544 | 544 | wp_localize_script( |
| 545 | 545 | "wz-{$this->prefix}-admin", |
| 546 | - 'WZSettingsAdmin', | |
| 546 | + 'WebberSettingsAdmin', | |
| 547 | 547 | array( |
| 548 | 548 | 'prefix' => $this->prefix, |
| 549 | 549 | 'settings_key' => $this->settings_key, |
| 550 | 550 | 'strings' => array( |
| @@ -605,9 +605,9 @@ | ||
| 605 | 605 | |
| 606 | 606 | // Localize Tom Select settings. |
| 607 | 607 | wp_localize_script( |
| 608 | 608 | "wz-{$prefix}-tom-select-init", |
| 609 | - 'WZTomSelectSettings', | |
| 609 | + 'WebberTomSelectSettings', | |
| 610 | 610 | $args |
| 611 | 611 | ); |
| 612 | 612 | wp_enqueue_script( "wz-{$prefix}-tom-select-init" ); |
| 613 | 613 | |
| @@ -657,9 +657,9 @@ | ||
| 657 | 657 | $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' ); |
| 658 | 658 | |
| 659 | 659 | // Tag header rows so the settings search can group fields under them. |
| 660 | 660 | if ( 'header' === $type ) { |
| 661 | - $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row' ); | |
| 661 | + $args['class'] = trim( ( $args['class'] ?? '' ) . ' wz-settings-header-row', " \t\n\r\0\x0B" ); | |
| 662 | 662 | } |
| 663 | 663 | |
| 664 | 664 | add_settings_field( |
| 665 | 665 | "{$settings_key}[{$id}]", // ID of the settings field. We save it within the settings array. |
| @@ -676,10 +676,13 @@ | ||
| 676 | 676 | register_setting( |
| 677 | 677 | $settings_key, |
| 678 | 678 | $settings_key, |
| 679 | 679 | array( |
| 680 | + 'type' => 'object', | |
| 681 | + 'default' => $this->settings_defaults(), | |
| 680 | 682 | 'sanitize_callback' => array( $this, 'settings_sanitize' ), |
| 681 | - '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, | |
| 682 | 685 | ) |
| 683 | 686 | ); |
| 684 | 687 | } |
| 685 | 688 | |
| @@ -814,28 +817,23 @@ | ||
| 814 | 817 | if ( $setting['id'] !== $key ) { |
| 815 | 818 | continue; |
| 816 | 819 | } |
| 817 | 820 | |
| 818 | - // Return the callback name. | |
| 819 | - $sanitize_callback = false; | |
| 820 | - | |
| 821 | 821 | if ( isset( $setting['sanitize_callback'] ) && is_callable( $setting['sanitize_callback'] ) ) { |
| 822 | - $sanitize_callback = $setting['sanitize_callback']; | |
| 823 | - return $sanitize_callback; | |
| 822 | + return $setting['sanitize_callback']; | |
| 824 | 823 | } |
| 825 | 824 | |
| 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; | |
| 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'; | |
| 835 | 830 | } |
| 836 | 831 | |
| 837 | - 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 | + }; | |
| 838 | 836 | } |
| 839 | 837 | } |
| 840 | 838 | |
| 841 | 839 | return false; |
| @@ -862,19 +860,19 @@ | ||
| 862 | 860 | |
| 863 | 861 | /** |
| 864 | 862 | * Sanitize the form data being submitted. |
| 865 | 863 | * |
| 866 | - * @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. | |
| 867 | 865 | * @return array Sanitized array |
| 868 | 866 | */ |
| 869 | 867 | public function settings_sanitize( $input ) { |
| 870 | - // This should be set if a form is submitted, so let's save it in the $referrer variable. | |
| 871 | - if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 872 | - 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 | |
| 873 | 873 | } |
| 874 | 874 | |
| 875 | - parse_str( sanitize_text_field( wp_unslash( $_POST['_wp_http_referer'] ) ), $referrer ); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 876 | - | |
| 877 | 875 | // Check if we need to set to defaults. |
| 878 | 876 | $reset = isset( $_POST['settings_reset'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 879 | 877 | |
| 880 | 878 | if ( $reset ) { |
| @@ -894,9 +892,9 @@ | ||
| 894 | 892 | |
| 895 | 893 | // Get the tab. This is also our settings' section. |
| 896 | 894 | $tab = $referrer['tab'] ?? $this->default_tab; |
| 897 | 895 | |
| 898 | - $input = $input ? $input : array(); | |
| 896 | + $input = is_array( $input ) ? $input : array(); | |
| 899 | 897 | |
| 900 | 898 | /** |
| 901 | 899 | * Filter the settings for the tab. e.g. prefix_settings_general_sanitize. |
| 902 | 900 | * |
| @@ -903,10 +901,10 @@ | ||
| 903 | 901 | * @param array $input Input unclean array |
| 904 | 902 | */ |
| 905 | 903 | $input = apply_filters( $this->prefix . '_settings_' . $tab . '_sanitize', $input ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound |
| 906 | 904 | |
| 907 | - // Create an output array by merging the existing settings with the ones submitted. | |
| 908 | - $output = array_merge( $settings, $input ); | |
| 905 | + // Start from what is stored. Submitted values are merged back in below, once sanitized. | |
| 906 | + $output = $settings; | |
| 909 | 907 | |
| 910 | 908 | // Loop through each setting being saved and pass it through a sanitization filter. |
| 911 | 909 | foreach ( $settings_types as $key => $type ) { |
| 912 | 910 | /** |
| @@ -924,13 +922,9 @@ | ||
| 924 | 922 | $sanitize_callback = $this->get_sanitize_callback( $key ); |
| 925 | 923 | |
| 926 | 924 | // If callback is set, call it. |
| 927 | 925 | if ( $sanitize_callback ) { |
| 928 | - if ( 'sensitive' === $type ) { | |
| 929 | - $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key ); | |
| 930 | - } else { | |
| 931 | - $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] ); | |
| 932 | - } | |
| 926 | + $output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] ); | |
| 933 | 927 | continue; |
| 934 | 928 | } |
| 935 | 929 | } |
| 936 | 930 | |
| @@ -940,13 +934,20 @@ | ||
| 940 | 934 | if ( ! isset( $locked[ $key ] ) ) { |
| 941 | 935 | unset( $output[ $key ] ); |
| 942 | 936 | } |
| 943 | 937 | } |
| 938 | + } | |
| 944 | 939 | |
| 945 | - // Delete any settings that are no longer part of our registered settings. | |
| 946 | - if ( array_key_exists( $key, $output ) && ! array_key_exists( $key, $settings_types ) ) { | |
| 947 | - unset( $output[ $key ] ); | |
| 948 | - } | |
| 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 ); | |
| 949 | 950 | } |
| 950 | 951 | |
| 951 | 952 | add_settings_error( $this->prefix . '-notices', '', $this->translation_strings['success_message'], 'updated' ); |
| 952 | 953 | |