Customizer APIs in 4.6 for Setting Validation and Notifications

As described in the Improving Setting Validation in the Customizer proposal post and detailed in #34893 and #36944, WordPress 4.6 includes new APIs related to validation of Customizer setting values. The Customizer has had sanitization of setting values since it was introduced. Sanitization involves coercing a value into something safe to persist to the database: common examples are converting a value into an integer or stripping tags from some text input. As such, sanitization is a lossy operation.

But what happens in sanitization if a provided value is irrecoverable, beyond the ability to sanitize? The Customizer did allow sanitizers to return null in such cases which resulted in the value being skipped entirely from previewing and saving, but there was no feedback to the user that the value was skipped. Additionally, when multiple settings were modified but some were skipped due to returning null, the result was that a save operation would only persist the non-skipped settings to database: a user would unexpectedly find that only some of their settings were applied, resulting in an inconsistent saved state. Save operations were not transactional/atomic.

These are the problems that the Customizer setting validation improvements in WordPress 4.6 are designed to address. With setting validation:

  1. All modified settings are validated up-front before any of them are saved.
  2. If any setting is invalid, the Customizer save request is rejected: a save thus becomes transactional with all the settings left dirty to try saving again. (The Customizer transactions proposal is closely related to setting validation here.)
  3. Validation error messages are displayed to the user, prompting them to fix their mistake and try again.

Sanitization and validation are also both part of the REST API infrastructure via WP_REST_Request::sanitize_params() and WP_REST_Request::validate_params(), respectively. A setting’s value goes through validation before it goes through sanitization (this is contrary to the original proposal, see #36944).

Validation Behavior

As noted above, if any setting is invalid, a Customizer save request is blocked. When save request is rejected due to setting invalidity, the controls that have the invalid setting will get the error notifications added to them, and one of these controls will be focused so that the user can correct the mistake. If there is an control with an invalid setting in the current section expanded, then this is the focus that will get the focus. Otherwise, the section containing an invalid control will get expanded and the control then focused.

Validation of setting values on the server does not only occur when a save is attempted. The setting values are validated and re-validated with each full refresh and selective refresh, and the their validity states are returned from the server in the responses. This means that setting validity will be reported in conjunction with updates to the preview. This is important for a couple reasons:

  1. When a setting is invalid, the previewed value will render using the previously-saved value or the default setting value, so the notification provides an explanation for why their changes aren’t appearing in the preview.
  2. As soon as a value is corrected and the preview request finishes, the error notification will be removed from the control. This means the user doesn’t have to try to saving to see whether the value has been corrected or not.

This second point has a key implication for validating settings that are previewed purely via JavaScript (the postMessage transport without selective refresh): you should perform the validation logic for these using JavaScript as well. See client-side validation below.

Adding Validation to a Setting

Validate Callback

Just as you can supply a sanitize_callback when registering a setting, you can also supply a validate_callback arg:

$wp_customize->add_setting( 'established_year', array(
    'sanitize_callback' => 'absint',
    'validate_callback' => 'validate_established_year'
) );
function validate_established_year( $validity, $value ) {
    $value = intval( $value );
    if ( empty( $value ) || ! is_numeric( $value ) ) {
        $validity->add( 'required', __( 'You must supply a valid year.' ) );
    } elseif ( $value < 1900 ) {
        $validity->add( 'year_too_small', __( 'Year is too old.' ) );
    } elseif ( $value > gmdate( 'Y' ) ) {
        $validity->add( 'year_too_big', __( 'Year is too new.' ) );
    }
    return $validity;
}

Just as supplying a sanitize_callback arg adds a filter for customize_sanitize_{$setting_id}, so too supplying a validate_callback arg will add a filter for customize_validate_{$setting_id}. Assuming that the WP_Customize_Setting instances apply filters on these in their validate methods, you can add this filter if you need to add validation for settings that have been previously added.

The validate_callback and any customize_validate_{$setting_id} filter callbacks take a WP_Error instance is its first argument (which initially is empty of any errors added), followed by the $value being sanitized, and lastly the WP_Customize_Setting instance that is being validated.

Validate Method

A second way to add validation to a setting is by overriding the validate method on a WP_Customize_Setting subclass. For example:

class Established_Year_Setting extends WP_Customize_Setting {
    function validate( $value ) {
        if ( empty( $value ) || ! is_numeric( $value ) ) {
            return new WP_Error( 'required', __( 'You must supply a valid year.' ) );
        }
        if ( $value < 1900 ) {
            return new WP_Error( 'year_too_small', __( 'Year is too old.' ) ); 
        } 
        if ( $value > gmdate( 'Y' ) ) {
            return new WP_Error( 'year_too_big', __( 'Year is too new.' ) );
        }
        return true;
    }
}

Validating via Sanitization

The last way to add validation to a setting is to overload the sanitization routine to include validation as well. Setting sanitization and setting validation are closely related. Sanitization is for coercing/cleaning data. Validation is a pass/fail operation and is key to prevent data from being accepted when it is “too far gone” to be recovered from or when it is undesirable for the value to be silently and lossily cleaned. As noted above, setting’s sanitization logic can continue to return null and this will get interpreted as a setting’s value being invalid with a returned generic “Invalid value” error. You may also return a WP_Error instance from a sanitization routine which can be a handy way to consolidate the closely-related sanitization/validation logic; if you do this, make sure that you opt to return null in WP≤4.5, as otherwise this would result in a WP_Error instance being saved as a setting’s value (!!):

function sanitize_number( $value ) {
    $can_validate = method_exists( 'WP_Customize_Setting', 'validate' );
    if ( ! is_numeric( $value ) ) {
        return $can_validate ? new WP_Error( 'nan', __( 'Not a number' ) ) : null;
    }
    return intval( $value );
}

Again, this useful if you want to consolidate the closely-related logic into a single routine and also when to perform late validation after a value has been sanitized.

Client-side Validation

As noted above, if you have a setting that is previewed purely via JavaScript (and the postMessage transport without selective refresh), you should also add client-side validation as well. If you don’t then any validation errors will persist until a refresh happens or a save is attempted. Client-side validation must not take the place of server-side validation, since it would be trivial for a malicious user to bypass the client-side validation to save an invalid value if corresponding server-side validation is not in place.

There is actually already a validate method available on the wp.customize.Setting JS class (actually, the wp.customize.Value base class). It was introduced with the inception of the Customizer in WP 3.4, although I believe it has been rarely utilized. The validate method’s name is a bit misleading, as it actually behaves very similarly to the WP_Customize_Setting::sanitize() PHP method. Nevertheless, the method can be used to both sanitize and validate a value in JS. Note that this JS runs in the context of the Customizer pane not the preview, so any such JS should have customize-controls as a dependency (not customize-preview) and enqueued during the customize_controls_enqueue_scripts action. Some example JS validation:

wp.customize( 'established_year', function ( setting ) {
	setting.validate = function ( value ) {
		var code, notification;
		var year = parseInt( value, 10 );

		code = 'required';
		if ( isNaN( year ) ) {
			notification = new wp.customize.Notification( code, {message: myPlugin.l10n.requiredYear} );
			setting.notifications.add( code, notification );
		} else {
			setting.notifications.remove( code );
		}

		if ( isNaN( year ) ) {
			return value;
		}

		code = 'year_too_small';
		if ( year < 1900 ) {
			notification = new wp.customize.Notification( code, {message: myPlugin.l10n.yearTooSmall} );
			setting.notifications.add( code, notification );
		} else {
			setting.notifications.remove( code );
		}

		code = 'year_too_big';
		if ( year > new Date().getFullYear() ) {
			notification = new wp.customize.Notification( code, {message: myPlugin.l10n.yearTooBig} );
			setting.notifications.add( code, notification );
		} else {
			setting.notifications.remove( code );
		}

		return value;
	};
} );

Notifications API

An error notification is added to a setting’s notifications collection when a setting’s validation routine returns a WP_Error instance. Each error added to a PHP WP_Error instance is represented as a wp.customize.Notification in JavaScript:

  • A WP_Error‘s code is available as notification.code in JS.
  • A WP_Error‘s message is available as notification.message in JS. Note that if there are multiple messages added to a given error code in PHP they will be concatenated into a single message in JS.
  • A WP_Error‘s data is available as notification.data in JS. This is useful to pass additional error context from the server to the client.

Any time that a WP_Error is returned from a validation routine on the server it will result in a wp.customize.Notification being created with a type property of “error”.

While setting non-error notifications from PHP is not currently supported (see #37281), you can also add non-error notifications with JS as follows:

wp.customize( 'blogname', function( setting ) {
    setting.bind( function( value ) {
        var code = 'long_title';
        if ( value.length > 20 ) {
            setting.notifications.add( code, new wp.customize.Notification(
                code,
                {
                    type: 'warning',
                    message: 'Theme prefers title with max 20 chars.'
                }
            ) );
        } else {
            setting.notifications.remove( code );
        }
    } );
} );

You can also supply “info” as a notification’s type. The default type is “error”. Custom types may also be supplied, and the notifications can be styled with CSS selector matching notice.notice-foo where “foo” is the type supplied. A control may also override the default behavior for how notifications are rendered by overriding the wp.customize.Control#renderNotifications method.

Error notification

Warning notification

Info notification

custom-notification

More Examples

A couple plugins that make use of validation:

  • Customize Validate Entitled Settings: Demo that forces the site title, widget titles, and nav menu labels to be populated with title case and lacking exclamations and questions.
  • Customize Posts: Validation errors added when posts would fail to save due to empty content, post locking, or save conflicts (with the specific conflicted fields being returned as WP_Error data). This plugin also features syncing back the server-sanitized setting values to upon save, something which I think should be in core at some point.
  • Standalone Customizer Controls: Demonstration of a controls used outside the Customizer with notifications added via JS based on HTML5 validity state.

Summary of API Changes

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.