Make WordPress Core


Ignore:
Timestamp:
05/20/2016 09:09:40 PM (9 years ago)
Author:
westonruter
Message:

Customize: Add setting validation model and control notifications to augment setting sanitization.

When a setting is invalid, not only will it be blocked from being saved but all other settings will be blocked as well. This ensures that Customizer saves aren't partial but are more transactional. User will be displayed the error in a notification so that they can fix and re-attempt saving.

PHP changes:

  • Introduces WP_Customize_Setting::validate(), WP_Customize_Setting::$validate_callback, and the customize_validate_{$setting_id} filter.
  • Introduces WP_Customize_Manager::validate_setting_values() to do validation (and sanitization) for the setting values supplied, returning a list of WP_Error instances for invalid settings.
  • Attempting to save settings that are invalid will result in the save being blocked entirely, with the errors being sent in the customize_save_response. Modifies WP_Customize_Manager::save() to check all settings for validity issues prior to calling their save methods.
  • Introduces WP_Customize_Setting::json() for parity with the other Customizer classes. This includes exporting of the type.
  • Modifies WP_Customize_Manager::post_value() to apply validate after sanitize, and if validation fails, to return the $default.
  • Introduces customize_save_validation_before action which fires right before the validation checks are made prior to saving.

JS changes:

  • Introduces wp.customize.Notification in JS which to represent WP_Error instances returned from the server when setting validation fails.
  • Introduces wp.customize.Setting.prototype.notifications.
  • Introduces wp.customize.Control.prototype.notifications, which are synced with a control's settings' notifications.
  • Introduces wp.customize.Control.prototype.renderNotifications() to re-render a control's notifications in its notification area. This is called automatically when the notifications collection changes.
  • Introduces wp.customize.settingConstructor, allowing custom setting types to be used in the same way that custom controls, panels, and sections can be made.
  • Injects a notification area into existing controls which is populated in response to the control's notifications collection changing. A custom control can customize the placement of the notification area by overriding the new getNotificationsContainerElement method.
  • When a save fails due to setting invalidity, the invalidity errors will be added to the settings to then populate in the controls' notification areas, and the first such invalid control will be focused.

Props westonruter, celloexpressions, mrahmadawais.
See #35210.
See #30937.
Fixes #34893.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r37342 r37476  
    655655     *
    656656     * @since 3.4.0
    657      * @since 4.1.1 Introduced 'default' parameter.
    658      *
    659      * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
    660      * @param mixed $default value returned $setting has no post value (added in 4.2.0).
    661      * @return string|mixed $post_value Sanitized value or the $default provided
     657     * @since 4.1.1 Introduced `$default` parameter.
     658     * @since 4.6.0 Return `$default` when setting post value is invalid.
     659     * @see WP_REST_Server::dispatch()
     660     * @see WP_Rest_Request::sanitize_params()
     661     * @see WP_Rest_Request::has_valid_params()
     662     *
     663     * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
     664     * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
     665     *                                      or the post value is invalid (added in 4.6.0).
     666     * @return string|mixed $post_value Sanitized value or the $default provided.
    662667     */
    663668    public function post_value( $setting, $default = null ) {
    664669        $post_values = $this->unsanitized_post_values();
    665         if ( array_key_exists( $setting->id, $post_values ) ) {
    666             return $setting->sanitize( $post_values[ $setting->id ] );
    667         } else {
     670        if ( ! array_key_exists( $setting->id, $post_values ) ) {
    668671            return $default;
    669672        }
     673        $value = $setting->sanitize( $post_values[ $setting->id ] );
     674        if ( is_null( $value ) || is_wp_error( $value ) ) {
     675            return $default;
     676        }
     677        $valid = $setting->validate( $value );
     678        if ( is_wp_error( $valid ) ) {
     679            return $default;
     680        }
     681        return $value;
    670682    }
    671683
     
    971983
    972984    /**
     985     * Validate setting values.
     986     *
     987     * Sanitization is applied to the values before being passed for validation.
     988     * Validation is skipped for unregistered settings or for values that are
     989     * already null since they will be skipped anyway.
     990     *
     991     * @since 4.6.0
     992     * @access public
     993     * @see WP_REST_Request::has_valid_params()
     994     *
     995     * @param array $setting_values Mapping of setting IDs to values to sanitize and validate.
     996     * @return array Empty array if all settings were valid. One or more instances of `WP_Error` if any were invalid.
     997     */
     998    public function validate_setting_values( $setting_values ) {
     999        $validity_errors = array();
     1000        foreach ( $setting_values as $setting_id => $unsanitized_value ) {
     1001            $setting = $this->get_setting( $setting_id );
     1002            if ( ! $setting || is_null( $unsanitized_value ) ) {
     1003                continue;
     1004            }
     1005            $validity = $setting->validate( $setting->sanitize( $unsanitized_value ) );
     1006            if ( false === $validity || null === $validity ) {
     1007                $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
     1008            }
     1009            if ( is_wp_error( $validity ) ) {
     1010                $validity_errors[ $setting_id ] = $validity;
     1011            }
     1012        }
     1013        return $validity_errors;
     1014    }
     1015
     1016    /**
    9731017     * Switch the theme and trigger the save() method on each setting.
    9741018     *
     
    9831027        if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
    9841028            wp_send_json_error( 'invalid_nonce' );
     1029        }
     1030
     1031        /**
     1032         * Fires before save validation happens.
     1033         *
     1034         * Plugins can add just-in-time `customize_validate_{$setting_id}` filters
     1035         * at this point to catch any settings registered after `customize_register`.
     1036         *
     1037         * @since 4.6.0
     1038         *
     1039         * @param WP_Customize_Manager $this WP_Customize_Manager instance.
     1040         */
     1041        do_action( 'customize_save_validation_before', $this );
     1042
     1043        // Validate settings.
     1044        $validity_errors = $this->validate_setting_values( $this->unsanitized_post_values() );
     1045        $invalid_count = count( $validity_errors );
     1046        if ( $invalid_count > 0 ) {
     1047            $settings_errors = array();
     1048            foreach ( $validity_errors as $setting_id => $validity_error ) {
     1049                $settings_errors[ $setting_id ] = array();
     1050                foreach ( $validity_error->errors as $error_code => $error_messages ) {
     1051                    $settings_errors[ $setting_id ][ $error_code ] = array(
     1052                        'message' => join( ' ', $error_messages ),
     1053                        'data' => $validity_error->get_error_data( $error_code ),
     1054                    );
     1055                }
     1056            }
     1057            $response = array(
     1058                'invalid_settings' => $settings_errors,
     1059                'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_count ), number_format_i18n( $invalid_count ) ),
     1060            );
     1061
     1062            /** This filter is documented in wp-includes/class-wp-customize-manager.php */
     1063            $response = apply_filters( 'customize_save_response', $response, $this );
     1064            wp_send_json_error( $response );
    9851065        }
    9861066
     
    14041484            $control->print_template();
    14051485        }
     1486        ?>
     1487        <script type="text/html" id="tmpl-customize-control-notifications">
     1488            <ul>
     1489                <# _.each( data.notifications, function( notification ) { #>
     1490                    <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{ notification.message || notification.code }}</li>
     1491                <# } ); #>
     1492            </ul>
     1493        </script>
     1494        <?php
    14061495    }
    14071496
     
    17641853                        "s[%s] = %s;\n",
    17651854                        wp_json_encode( $setting->id ),
    1766                         wp_json_encode( array(
    1767                             'value'     => $setting->js_value(),
    1768                             'transport' => $setting->transport,
    1769                             'dirty'     => $setting->dirty,
    1770                         ) )
     1855                        wp_json_encode( $setting->json() )
    17711856                    );
    17721857                }
Note: See TracChangeset for help on using the changeset viewer.