Skip to content

Instantly share code, notes, and snippets.

@westonruter
Last active November 19, 2017 15:09
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save westonruter/1016332b18ee7946dec3 to your computer and use it in GitHub Desktop.
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