Plugin demonstration of Customize Setting Validation: validates Customizer settings that contain titles to ensure they are not empty, and then blocks saving the Customizer until they are populated. See https://github.com/xwp/wp-customize-setting-validation
/* global wp, _customizeValidateEntitledSettingsExports */ | |
/* exported customizeValidateEntitledSettings */ | |
var customizeValidateEntitledSettings = ( function( $, api, exports ) { | |
var self = { | |
l10n: { | |
empty_title_invalidity: '' | |
} | |
}; | |
if ( exports ) { | |
$.extend( self, exports ); | |
} | |
/** | |
* Add validation to a control if it is entitled (has a title or is a title). | |
* | |
* @param {wp.customize.Control} setting - Control. | |
* @param {wp.customize.Value} setting.validationMessage - Validation message. | |
* @return {boolean} Whether validation was added. | |
*/ | |
self.addValidationForEntitledSetting = function( setting ) { | |
var initialValidate; | |
if ( ! self.isEntitledSetting( setting ) ) { | |
return false; | |
} | |
initialValidate = setting.validate; | |
/** | |
* Wrap the setting's validate() method to do validation on the value to be sent to the server. | |
* | |
* @param {mixed} newValue - New value being assigned to the setting. | |
* @returns {*} | |
*/ | |
setting.validate = function( newValue ) { | |
var setting = this, title, validationError; | |
// Note: if we want to get the old value, just do oldValue = this.get() | |
if ( _.isObject( newValue ) ) { | |
title = newValue.title; | |
} else { | |
title = newValue; | |
} | |
newValue = initialValidate.call( this, newValue ); | |
if ( '' === jQuery.trim( title ) ) { | |
validationError = new api.Notification( 'empty_title_invalidity', { message: self.l10n.empty_title_invalidity } ); | |
setting.notifications.add( validationError.code, validationError ); | |
} else { | |
setting.notifications.remove( 'empty_title_invalidity' ); | |
} | |
return newValue; | |
}; | |
return true; | |
}; | |
/** | |
* Return whether the setting is entitled (i.e. if it is a title or has a title). | |
* | |
* @param {wp.customize.Setting} setting - Setting. | |
* @returns {boolean} | |
*/ | |
self.isEntitledSetting = function( setting ) { | |
return ( | |
'blogname' === setting.id | |
); | |
}; | |
api.bind( 'add', function( setting ) { | |
self.addValidationForEntitledSetting( setting ); | |
} ); | |
return self; | |
}( jQuery, wp.customize, _customizeValidateEntitledSettingsExports ) ); |
<?php | |
/** | |
* Plugin name: Customize Validate Entitled Settings | |
* Description: Prevent the site title, nav menu items, and widget instances from being saved without titles. Enforces title case via sanitization. Depends on patch from <a href="https://core.trac.wordpress.org/ticket/34893">#34893</a> plugin. | |
* Author: Weston Ruter, XWP. | |
* Plugin URL: https://gist.github.com/westonruter/1016332b18ee7946dec3 | |
* Version: 0.2 | |
* Author: XWP | |
* Author URI: https://xwp.co/ | |
* License: GPLv2+ | |
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt | |
* Text Domain: customize-validate-entitled-settings | |
* | |
* Copyright (c) 2016 XWP (https://xwp.co/) | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License, version 2 or, at | |
* your discretion, any later version, as published by the Free | |
* Software Foundation. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program; if not, write to the Free Software | |
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
*/ | |
/** | |
* Class Customize_Validate_Entitled_Settings | |
*/ | |
class Customize_Validate_Entitled_Settings { | |
/** | |
* Translated strings | |
* | |
* @var array | |
*/ | |
public $l10n = array(); | |
/** | |
* Add hooks. | |
*/ | |
public function init() { | |
require_once ABSPATH . WPINC . '/class-wp-customize-setting.php'; | |
if ( ! method_exists( 'WP_Customize_Setting', 'validate' ) ) { | |
add_action( 'admin_notices', array( $this, 'show_admin_notice_for_missing_patch' ) ); | |
return; | |
} | |
add_action( 'customize_register', array( $this, 'add_filters_for_setting_validation' ), 100 ); | |
// Prevent performing validation during update-widget request so that form can actually return with a full response. | |
// @todo This has only been needed since 4.8 and it seems to only be an issue for the Text widget. More investigation is needed. | |
$is_update_widgets_request = ( wp_doing_ajax() && isset( $_REQUEST['action'] ) && 'update-widget' === $_REQUEST['action'] ); | |
if ( ! $is_update_widgets_request ) { | |
add_filter( 'widget_customizer_setting_args', array( $this, 'filter_widget_customizer_setting_args' ), 10, 2 ); | |
} | |
add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) ); | |
$this->l10n = array( | |
'empty_title_invalidity' => __( 'You must supply a title/label.', 'customize-validate-entitled-settings' ), | |
'shouting_title_invalidity' => __( 'YOU ARE NOT ALLOWED TO SHOUT.', 'customize-validate-entitled-settings' ), | |
'indecisive_title_invalidity' => __( 'Why not avoid questions in titles/labels?', 'customize-validate-entitled-settings' ), | |
); | |
} | |
/** | |
* Show admin notice when patch from #34893 is not applied. | |
*/ | |
public function show_admin_notice_for_missing_patch() { | |
?> | |
<div class="error"> | |
<p>The <strong>Customize Validate Entitled Settings</strong> plugin requires the patch from <a href="https://core.trac.wordpress.org/ticket/34893">#34893</a> to be applied.</p> | |
</div> | |
<?php | |
} | |
/** | |
* Enqueue scripts. | |
* | |
* @action customize_controls_enqueue_scripts | |
*/ | |
public function customize_controls_enqueue_scripts() { | |
$handle = 'customize-validate-entitled-setting'; | |
$src = plugin_dir_url( __FILE__ ) . 'customize-validate-entitled-settings.js'; | |
$deps = array( 'customize-controls' ); | |
wp_enqueue_script( $handle, $src, $deps ); | |
$exports = array( | |
'l10n' => $this->l10n, | |
); | |
wp_scripts()->add_data( $handle, 'data', sprintf( 'var _customizeValidateEntitledSettingsExports = %s;', wp_json_encode( $exports ) ) ); | |
} | |
/** | |
* Add filters for setting validation. | |
* | |
* @param WP_Customize_Manager $wp_customize Customize manager. | |
*/ | |
public function add_filters_for_setting_validation( $wp_customize ) { | |
add_filter( 'customize_sanitize_blogname', array( $this, 'sanitize_title' ) ); | |
add_filter( 'customize_validate_blogname', array( $this, 'validate_title' ), 10, 3 ); | |
// Add filters to all nav menu items and widget instances. | |
foreach ( $wp_customize->settings() as $setting ) { | |
if ( $setting instanceof WP_Customize_Nav_Menu_Item_Setting || $setting instanceof WP_Customize_Post_Setting ) { | |
add_filter( "customize_sanitize_{$setting->id}", array( $this, 'sanitize_array_containing_title' ) ); | |
add_filter( "customize_validate_{$setting->id}", array( $this, 'validate_array_containing_title' ), 10, 3 ); | |
} | |
} | |
} | |
/** | |
* Side-load filters for sanitizing and validating widget settings. | |
* | |
* This method of adding filters is somewhat hacky. It is necessary due to | |
* incoming dirty settings being previewed immediately after the settings | |
* dynamic widget settings are created. | |
* | |
* In the future a widget should handle this via implementing `WP_JS_Widget::validate()`. | |
* | |
* @link https://github.com/xwp/wp-js-widgets/pull/8/files#diff-42d2172baac53c364a4b33471b86c9d5R275 | |
* | |
* @see WP_Customize_Widgets::register_settings() | |
* | |
* @param array $args Setting args. | |
* @param string $id Setting ID. | |
* @return array Args unmodified. | |
*/ | |
public function filter_widget_customizer_setting_args( $args, $id ) { | |
$prefix = 'widget_'; | |
if ( substr( $id, 0, strlen( $prefix ) ) === $prefix ) { | |
/* | |
* Note that the sanitize filter needs to be higher than 10 which is | |
* the priority at which a JS-sanitized encoded-serialized instance | |
* is converted to its internal array representation. This only | |
* applies to legacy widgets that do not extend WP_JS_Widget. | |
* The priority of 10 is specified in `WP_Customize_Setting::__construct()` | |
* where the filter for `customize_sanitize_{$id}` is added for the | |
* setting's `sanitize_callback`. | |
*/ | |
$priority = 20; | |
add_filter( "customize_sanitize_{$id}", array( $this, 'sanitize_array_containing_title' ), $priority, 2 ); | |
add_filter( "customize_validate_{$id}", array( $this, 'validate_array_containing_title' ), $priority, 3 ); | |
} | |
return $args; | |
} | |
/** | |
* Validate a title field after sanitizing with ucwords() for demonstration purposes. | |
* | |
* @param string $value Title. | |
* @return string String if valid, error if not. | |
*/ | |
public function sanitize_title( $value ) { | |
$value = sanitize_text_field( $value ); | |
$value = ucwords( $value ); | |
return $value; | |
} | |
/** | |
* Validate a (sanitized) title to not be empty. | |
* | |
* @param WP_Error $validity Validity. | |
* @param string $value Value, normally pre-sanitized. | |
* @param string $setting Setting. | |
* @return WP_Error | |
*/ | |
public function validate_title( $validity, $value, $setting ) { | |
$data = array(); | |
if ( $setting instanceof WP_Customize_Post_Setting ) { | |
$data['setting_property'] = 'post_title'; | |
} | |
if ( '' === $value ) { | |
$validity->add( 'empty_title_invalidity', $this->l10n['empty_title_invalidity'], $data ); | |
} | |
if ( false !== strpos( $value, '!' ) ) { | |
$validity->add( 'shouting_title_invalidity', $this->l10n['shouting_title_invalidity'], $data ); | |
} | |
if ( false !== strpos( $value, '?' ) ) { | |
$validity->add( 'indecisive_title_invalidity', $this->l10n['indecisive_title_invalidity'], $data ); | |
} | |
return $validity; | |
} | |
/** | |
* Sanitize an array containing a title property. | |
* | |
* @param array $data { | |
* Array containing a title. | |
* | |
* @type string [$title] Title. | |
* } | |
* | |
* @return array|WP_Error Returns sanitized array if title is valid, WP_Error otherwise. | |
*/ | |
public function sanitize_array_containing_title( $data ) { | |
if ( is_array( $data ) ) { | |
if ( array_key_exists( 'title', $data ) ) { | |
$data['title'] = $this->sanitize_title( $data['title'] ); | |
} elseif ( array_key_exists( 'post_title', $data ) ) { | |
$data['post_title'] = $this->sanitize_title( $data['post_title'] ); | |
} | |
} | |
return $data; | |
} | |
/** | |
* Validate an array containing a title property. | |
* | |
* @param null|true|WP_Error $validity Validity. | |
* @param array $data { | |
* Array containing a title. | |
* | |
* @type string [$title] Title. | |
* } | |
* @param WP_Customize_Setting $setting Setting. | |
* | |
* @return array|WP_Error Returns sanitized array if title is valid, WP_Error otherwise. | |
*/ | |
public function validate_array_containing_title( $validity, $data, $setting = null ) { | |
if ( is_array( $data ) ) { | |
if ( $setting instanceof WP_Customize_Nav_Menu_Item_Setting ) { | |
$can_use_original_title = ( ! empty( $data['type'] ) && 'post_type_archive' === $data['type'] || ! empty( $data['object_id'] ) ); | |
if ( empty( $data['title'] ) && ! $can_use_original_title ) { | |
$validity = $this->validate_title( $validity, '', $setting ); | |
} | |
} elseif ( array_key_exists( 'title', $data ) ) { | |
$validity = $this->validate_title( $validity, $data['title'], $setting ); | |
} elseif ( $setting instanceof WP_Customize_Post_Setting && 'trash' !== $data['post_status'] ) { | |
$validity = $this->validate_title( $validity, $data['post_title'], $setting ); | |
} | |
} | |
return $validity; | |
} | |
/** | |
* Filter a widget's settings before saving. | |
* | |
* Returning false will effectively short-circuit the widget's ability | |
* to update settings. | |
* | |
* This was added in init() via `add_filter( 'widget_update_callback', array( $this, 'sanitize_and_validate_widget_instance' ), 10, 4 );` | |
* It was disabled, however, because it provides a poor experience in that it | |
* just blocks a form edit from sticking and it doesn't allow any invalid | |
* setting values to be submitted, and thus no opportunity for rejection and | |
* displaying of the invalidity message. | |
* | |
* @param array $instance The current widget instance's settings. | |
* @param array $new_instance Array of new widget settings. | |
* @param array $old_instance Array of old widget settings. | |
* @return array|false | |
*/ | |
public function sanitize_and_validate_widget_instance( $instance, $new_instance, $old_instance ) { | |
unset( $new_instance, $old_instance ); | |
if ( ! isset( $instance['title'] ) ) { | |
$instance['title'] = ''; | |
} | |
$instance = $this->sanitize_array_containing_title( $instance ); | |
$validity = $this->validate_array_containing_title( new WP_Error(), $instance ); | |
if ( ! empty( $validity->errors ) ) { | |
return false; | |
} | |
return $instance; | |
} | |
} | |
$customize_validate_entitled_settings_plugin = new Customize_Validate_Entitled_Settings(); | |
add_action( 'plugins_loaded', array( $customize_validate_entitled_settings_plugin, 'init' ) ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment