├── .coveralls.yml
├── .david-dev
├── .dev-lib
├── .gitignore
├── .gitmodules
├── .jscsrc
├── .jshintignore
├── .jshintrc
├── .travis.yml
├── Gruntfile.js
├── composer.json
├── contributing.md
├── css
└── customize-setting-validation.css
├── customize-setting-validation.php
├── instance.php
├── js
└── customize-setting-validation.js
├── package.json
├── php
├── class-exception.php
├── class-plugin-base.php
└── class-plugin.php
├── phpcs.ruleset.xml
├── phpunit.xml.dist
├── readme.md
├── readme.txt
├── tests
├── php
│ ├── test-class-plugin-base.php
│ └── test-class-plugin.php
└── test-customize-setting-validation.php
└── wp-assets
├── banner-1544x500.png
├── banner-772x250.png
├── banner.svg
├── icon-128x128.png
├── icon-256x256.png
├── icon.svg
├── screenshot-1.png
├── screenshot-2.png
└── screenshot-3.png
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 | src_dir: .
3 | coverage_clover: build/logs/clover.xml
4 | json_path: build/logs/coveralls-upload.json
5 |
--------------------------------------------------------------------------------
/.david-dev:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xwp/wp-customize-setting-validation/2e5ddc66a870ad7b1aee5f8e414bad4b78e120d2/.david-dev
--------------------------------------------------------------------------------
/.dev-lib:
--------------------------------------------------------------------------------
1 | PATH_INCLUDES='*.php *.js *.json php/* tests/* js/* css/*'
2 | PHPCS_IGNORE=vendor
3 | SVN_URL=https://plugins.svn.wordpress.org/customize-setting-validation/
4 | ASSETS_DIR=wp-assets
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # Grunt
4 | /build/
5 | /node_modules/
6 | npm-debug.log
7 | /vendor
8 | *.min.css
9 | *.min.js
10 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "dev-lib"]
2 | path = dev-lib
3 | url = https://github.com/xwp/wp-dev-lib.git
4 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | dev-lib/.jscsrc
--------------------------------------------------------------------------------
/.jshintignore:
--------------------------------------------------------------------------------
1 | dev-lib/.jshintignore
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | dev-lib/.jshintrc
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language:
4 | - php
5 | - node_js
6 |
7 | php:
8 | - 5.4
9 | - 7.0
10 |
11 | node_js:
12 | - 0.10
13 |
14 | env:
15 | - WP_VERSION=latest WP_MULTISITE=0
16 | - WP_VERSION=latest WP_MULTISITE=1
17 | - WP_VERSION=trunk WP_MULTISITE=0
18 | - WP_VERSION=trunk WP_MULTISITE=1
19 |
20 | install:
21 | - export DEV_LIB_PATH=dev-lib
22 | - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi
23 | - if [ ! -e "$DEV_LIB_PATH" ]; then git clone https://github.com/xwp/wp-dev-lib.git $DEV_LIB_PATH; fi
24 | - source $DEV_LIB_PATH/travis.install.sh
25 |
26 | script:
27 | - source $DEV_LIB_PATH/travis.script.sh
28 |
29 | after_script:
30 | - source $DEV_LIB_PATH/travis.after_script.sh
31 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* jshint node:true */
3 |
4 | module.exports = function( grunt ) {
5 | 'use strict';
6 |
7 | grunt.initConfig( {
8 |
9 | pkg: grunt.file.readJSON( 'package.json' ),
10 |
11 | // JavaScript linting with JSHint.
12 | jshint: {
13 | options: {
14 | jshintrc: '.jshintrc'
15 | },
16 | all: [
17 | 'Gruntfile.js',
18 | 'js/*.js',
19 | '!js/*.min.js'
20 | ]
21 | },
22 |
23 | // Minify .js files.
24 | uglify: {
25 | options: {
26 | preserveComments: false
27 | },
28 | core: {
29 | files: [ {
30 | expand: true,
31 | cwd: 'js/',
32 | src: [
33 | '*.js',
34 | '!*.min.js'
35 | ],
36 | dest: 'js/',
37 | ext: '.min.js'
38 | } ]
39 | }
40 | },
41 |
42 | // Minify .css files.
43 | cssmin: {
44 | core: {
45 | files: [ {
46 | expand: true,
47 | cwd: 'css/',
48 | src: [
49 | '*.css',
50 | '!*.min.css'
51 | ],
52 | dest: 'css/',
53 | ext: '.min.css'
54 | } ]
55 | }
56 | },
57 |
58 | // Build a deploy-able plugin
59 | copy: {
60 | build: {
61 | src: [
62 | '*.php',
63 | 'css/*',
64 | 'js/*',
65 | 'php/*',
66 | 'readme.txt'
67 | ],
68 | dest: 'build',
69 | expand: true,
70 | dot: true
71 | }
72 | },
73 |
74 | // Clean up the build
75 | clean: {
76 | build: {
77 | src: [ 'build' ]
78 | }
79 | },
80 |
81 | // VVV (Varying Vagrant Vagrants) Paths
82 | vvv: {
83 | 'plugin': '/srv/www/wordpress-develop/src/wp-content/plugins/<%= pkg.name %>',
84 | 'coverage': '/srv/www/default/coverage/<%= pkg.name %>'
85 | },
86 |
87 | // Shell actions
88 | shell: {
89 | options: {
90 | stdout: true,
91 | stderr: true
92 | },
93 | readme: {
94 | command: 'cd ./dev-lib && ./generate-markdown-readme' // Generate the readme.md
95 | },
96 | phpunit: {
97 | command: 'vagrant ssh -c "cd <%= vvv.plugin %> && phpunit"'
98 | },
99 | phpunit_c: {
100 | command: 'vagrant ssh -c "cd <%= vvv.plugin %> && phpunit --coverage-html <%= vvv.coverage %>"'
101 | }
102 | },
103 |
104 | // Deploys a git Repo to the WordPress SVN repo
105 | wp_deploy: {
106 | deploy: {
107 | options: {
108 | plugin_slug: '<%= pkg.name %>',
109 | build_dir: 'build',
110 | assets_dir: 'wp-assets'
111 | }
112 | }
113 | }
114 |
115 | } );
116 |
117 | // Load tasks
118 | grunt.loadNpmTasks( 'grunt-contrib-clean' );
119 | grunt.loadNpmTasks( 'grunt-contrib-copy' );
120 | grunt.loadNpmTasks( 'grunt-contrib-cssmin' );
121 | grunt.loadNpmTasks( 'grunt-contrib-jshint' );
122 | grunt.loadNpmTasks( 'grunt-contrib-uglify' );
123 | grunt.loadNpmTasks( 'grunt-shell' );
124 | grunt.loadNpmTasks( 'grunt-wp-deploy' );
125 |
126 | // Register tasks
127 | grunt.registerTask( 'default', [
128 | 'jshint',
129 | 'uglify',
130 | 'cssmin'
131 | ] );
132 |
133 | grunt.registerTask( 'readme', [
134 | 'shell:readme'
135 | ] );
136 |
137 | grunt.registerTask( 'phpunit', [
138 | 'shell:phpunit'
139 | ] );
140 |
141 | grunt.registerTask( 'phpunit_c', [
142 | 'shell:phpunit_c'
143 | ] );
144 |
145 | grunt.registerTask( 'dev', [
146 | 'default',
147 | 'readme'
148 | ] );
149 |
150 | grunt.registerTask( 'build', [
151 | 'default',
152 | 'readme',
153 | 'copy'
154 | ] );
155 |
156 | grunt.registerTask( 'deploy', [
157 | 'build',
158 | 'wp_deploy',
159 | 'clean'
160 | ] );
161 |
162 | };
163 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require-dev": {
3 | "satooshi/php-coveralls": "dev-master"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Customize Setting Validation Contributing Guide
2 |
3 | Before submitting your contribution, please make sure to take a moment and read through the following guidelines.
4 |
5 | ## Issue Reporting Guidelines
6 |
7 | - The issue list of this repo is **exclusively** for bug reports and feature requests.
8 | - Try to search for your issue, it may have already been answered or even fixed in the `wip` (Work in Progress) branch.
9 | - Check if the issue is reproducible with the latest stable version. If you are using a pre-release, please indicate the specific version you are using.
10 | - It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Issues without clear reproducible steps will be closed immediately.
11 | - If your issue is resolved but still open, don't hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it.
12 |
13 | ## Pull Request Guidelines
14 |
15 | - Checkout a topic branch from `wip` and merge back against `wip`.
16 | - If you are not familiar with branching please read [_A successful Git branching model_](http://nvie.com/posts/a-successful-git-branching-model/) before you go any further.
17 | - **DO NOT** check-in the `build` directory with your commits.
18 | - Follow the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/coding-standards/).
19 | - Make sure the default grunt task passes. (see [development setup](#development-setup))
20 | - If adding a new feature:
21 | - Add accompanying test case.
22 | - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it green-lit before working on it.
23 | - If fixing a bug:
24 | - Provide detailed description of the bug in the PR. Live demo preferred.
25 | - Add appropriate test coverage if applicable.
26 |
27 | ## Development Setup
28 |
29 | You will need [Node.js](http://nodejs.org), [Grunt](http://gruntjs.com), & [PHPUnit](https://phpunit.de/getting-started.html) installed on your system. To run the unit tests you must be developing within the WordPress Core. The simplest method to get a testing environment up is by using [Varying Vagrant Vagrants](https://github.com/Varying-Vagrant-Vagrants/VVV). However, if you are using MAMP then the following command will clone `trunk`.
30 |
31 | To clone the WordPress Core
32 |
33 | ``` bash
34 | $ git clone https://github.com/xwp/wordpress-develop.git
35 | ```
36 |
37 | To clone this repository
38 | ``` bash
39 | $ git clone --recursive git@github.com:xwp/wp-customize-setting-validation.git customize-setting-validation
40 | ```
41 |
42 | To install packages
43 |
44 | ``` bash
45 | # npm install -g grunt-cli
46 | $ npm install
47 | ```
48 |
49 | To lint:
50 |
51 | ``` bash
52 | $ grunt jshint
53 | ```
54 |
55 | To check the text domain:
56 |
57 | ``` bash
58 | $ grunt checktextdomain
59 | ```
60 |
61 | To create a pot file:
62 |
63 | ``` bash
64 | $ grunt makepot
65 | ```
66 |
67 | The default task (simply running `grunt`) will do the following: `jshint -> checktextdomain`.
68 |
69 | ### PHPUnit Testing
70 |
71 | Run tests:
72 |
73 | ``` bash
74 | $ phpunit
75 | ```
76 |
77 | Run tests with an HTML coverage report:
78 |
79 | ``` bash
80 | $ phpunit --coverage-html /tmp/report
81 | ```
82 |
83 | Travis CI will run the unit tests and perform sniffs against the WordPress Coding Standards whenever you push changes to your PR. Tests are required to pass successfully for a merge to be considered.
--------------------------------------------------------------------------------
/css/customize-setting-validation.css:
--------------------------------------------------------------------------------
1 |
2 | div.customize-setting-validation-message.error {
3 | padding-top: 0.5em;
4 | padding-bottom: 0.5em;
5 | display: none;
6 | }
7 |
8 | .customize-control-widget_form.customize-setting-invalid .widget .widget-top {
9 | background-color: #FFE0E0;
10 | }
11 | .customize-control-nav_menu_item.customize-setting-invalid .menu-item-bar .menu-item-handle {
12 | background-color: #FFE0E0;
13 | }
14 |
15 | .customize-setting-validation-message li {
16 | list-style: disc;
17 | margin-bottom: 0;
18 | margin-left: 1em;
19 | }
20 | .customize-setting-validation-message li:only-child {
21 | list-style: none;
22 | margin-left: 0;
23 | }
24 |
--------------------------------------------------------------------------------
/customize-setting-validation.php:
--------------------------------------------------------------------------------
1 | =' ) ) {
33 | require_once __DIR__ . '/instance.php';
34 | } else {
35 | if ( defined( 'WP_CLI' ) ) {
36 | WP_CLI::warning( _customize_setting_validation_php_version_text() );
37 | } else {
38 | add_action( 'admin_notices', '_customize_setting_validation_php_version_error' );
39 | }
40 | }
41 |
42 | /**
43 | * Admin notice for incompatible versions of PHP.
44 | */
45 | function _customize_setting_validation_php_version_error() {
46 | printf( '
%s
', esc_html( _customize_setting_validation_php_version_text() ) );
47 | }
48 |
49 | /**
50 | * String describing the minimum PHP version.
51 | *
52 | * @return string
53 | */
54 | function _customize_setting_validation_php_version_text() {
55 | return __( 'Customize Setting Validation plugin error: Your version of PHP is too old to run this plugin. You must be running PHP 5.3 or higher.', 'customize-setting-validation' );
56 | }
57 |
--------------------------------------------------------------------------------
/instance.php:
--------------------------------------------------------------------------------
1 | ' );
110 |
111 | if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
112 | control.container.find( '.menu-item-settings:first' ).prepend( validationMessageElement );
113 | } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
114 | control.container.find( '.widget-inside:first' ).prepend( validationMessageElement );
115 | } else {
116 | controlTitle = control.container.find( '.customize-control-title' );
117 | if ( controlTitle.length ) {
118 | controlTitle.after( validationMessageElement );
119 | } else {
120 | control.container.append( validationMessageElement );
121 | }
122 | }
123 | return validationMessageElement;
124 | };
125 |
126 | /**
127 | * Reset the validation messages and capture the settings that were dirty.
128 | */
129 | self.beforeSave = function() {
130 | api.each( function( setting ) {
131 | if ( setting.validationMessage ) {
132 | setting.validationMessage.set( '' );
133 | }
134 | } );
135 | };
136 |
137 | /**
138 | * Handle a failure to save the Customizer settings.
139 | *
140 | * @param {object} response - Data sent back by customize-save Ajax request.
141 | * @param {array} [response.invalid_settings] - IDs for invalid settings mapped to the validation messages.
142 | */
143 | self.afterSaveFailure = function( response ) {
144 | var invalidControls = [], wasFocused = false;
145 | if ( ! response.invalid_settings || 0 === response.invalid_settings.length ) {
146 | return;
147 | }
148 |
149 | // Find the controls that correspond to each invalid setting.
150 | _.each( response.invalid_settings, function( invalidMessage, settingId ) {
151 | var setting = api( settingId );
152 | if ( setting ) {
153 | setting.validationMessage.set( invalidMessage );
154 | }
155 |
156 | api.control.each( function( control ) {
157 | _.each( control.settings, function( controlSetting ) {
158 | if ( controlSetting.id === settingId ) {
159 | invalidControls.push( control );
160 | }
161 | } );
162 | } );
163 | } );
164 |
165 | // Focus on the first control that is inside of an expanded section (one that is visible).
166 | _( invalidControls ).find( function( control ) {
167 | var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
168 | if ( isExpanded && control.expanded ) {
169 | isExpanded = control.expanded();
170 | }
171 | if ( isExpanded ) {
172 | control.focus();
173 | wasFocused = true;
174 | }
175 | return wasFocused;
176 | } );
177 |
178 | // Focus on the first invalid control.
179 | if ( ! wasFocused && invalidControls[0] ) {
180 | invalidControls[0].focus();
181 | }
182 |
183 | // @todo Also display response.message somewhere.
184 | };
185 |
186 | /**
187 | * Apply saved sanitized values from server to settings in JS client, if different.
188 | *
189 | * @param {object} response
190 | * @param {object} response.sanitized_setting_values
191 | */
192 | self.afterSaveSuccess = function( response ) {
193 | var wasSaved;
194 | if ( ! response.sanitized_setting_values ) {
195 | return;
196 | }
197 |
198 | wasSaved = api.state( 'saved' ).get();
199 |
200 | _.each( response.sanitized_setting_values, function( value, id ) {
201 | var setting = api( id );
202 | if ( setting ) {
203 | setting.set( value );
204 | setting._dirty = false;
205 | }
206 | } );
207 |
208 | api.state( 'saved' ).set( wasSaved );
209 | };
210 |
211 | api.bind( 'add', function( setting ) {
212 | self.setupSettingForValidationMessage( setting );
213 | } );
214 | api.control.bind( 'add', function( control ) {
215 | self.setupControlForValidationMessage( control );
216 | } );
217 | api.bind( 'save', function() {
218 | self.beforeSave();
219 | } );
220 | api.bind( 'error', function( response ) {
221 | self.afterSaveFailure( response );
222 | } );
223 | api.bind( 'saved', function( response ) {
224 | self.afterSaveSuccess( response );
225 | } );
226 |
227 | return self;
228 |
229 | }( jQuery, wp.customize ) );
230 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "customize-setting-validation",
3 | "title": "Customize Setting Validation",
4 | "homepage": "https://github.com/xwp/wp-customize-setting-validation",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/xwp/wp-customize-setting-validation.git"
8 | },
9 | "author": "XWP",
10 | "license": "GPL-2.0+",
11 | "devDependencies": {
12 | "grunt": "~1.0.1",
13 | "grunt-checktextdomain": "~1.0.0",
14 | "grunt-contrib-clean": "^1.0.0",
15 | "grunt-contrib-copy": "~1.0.0",
16 | "grunt-contrib-cssmin": "^1.0.1",
17 | "grunt-contrib-jshint": "~1.0.0",
18 | "grunt-contrib-uglify": "^1.0.1",
19 | "grunt-shell": "~1.3.0",
20 | "grunt-wp-deploy": "^1.1.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/php/class-exception.php:
--------------------------------------------------------------------------------
1 | locate_plugin();
55 | $this->slug = $location['dir_basename'];
56 | $this->dir_path = $location['dir_path'];
57 | $this->dir_url = $location['dir_url'];
58 | spl_autoload_register( array( $this, 'autoload' ) );
59 | }
60 |
61 | /**
62 | * Get reflection object for this class.
63 | *
64 | * @return \ReflectionObject
65 | */
66 | public function get_object_reflection() {
67 | static $reflection;
68 | if ( empty( $reflection ) ) {
69 | $reflection = new \ReflectionObject( $this );
70 | }
71 | return $reflection;
72 | }
73 |
74 | /**
75 | * Autoload matches cache.
76 | *
77 | * @var array
78 | */
79 | protected $autoload_matches_cache = array();
80 |
81 | /**
82 | * Autoload for classes that are in the same namespace as $this.
83 | *
84 | * @param string $class Class name.
85 | * @return void
86 | */
87 | public function autoload( $class ) {
88 | if ( ! isset( $this->autoload_matches_cache[ $class ] ) ) {
89 | if ( ! preg_match( '/^(?P.+)\\\\(?P[^\\\\]+)$/', $class, $matches ) ) {
90 | $matches = false;
91 | }
92 | $this->autoload_matches_cache[ $class ] = $matches;
93 | } else {
94 | $matches = $this->autoload_matches_cache[ $class ];
95 | }
96 | if ( empty( $matches ) ) {
97 | return;
98 | }
99 | if ( $this->get_object_reflection()->getNamespaceName() !== $matches['namespace'] ) {
100 | return;
101 | }
102 | $class_name = $matches['class'];
103 |
104 | $class_path = \trailingslashit( $this->dir_path );
105 | if ( $this->autoload_class_dir ) {
106 | $class_path .= \trailingslashit( $this->autoload_class_dir );
107 | }
108 | $class_path .= sprintf( 'class-%s.php', strtolower( str_replace( '_', '-', $class_name ) ) );
109 | if ( is_readable( $class_path ) ) {
110 | require_once $class_path;
111 | }
112 | }
113 |
114 | /**
115 | * Version of plugin_dir_url() which works for plugins installed in the plugins directory,
116 | * and for plugins bundled with themes.
117 | *
118 | * @throws \Exception If the plugin is not located in the expected location.
119 | * @return array
120 | */
121 | public function locate_plugin() {
122 | $file_name = $this->get_object_reflection()->getFileName();
123 | if ( '/' !== \DIRECTORY_SEPARATOR ) {
124 | $file_name = str_replace( \DIRECTORY_SEPARATOR, '/', $file_name ); // Windows compat.
125 | }
126 | $plugin_dir = preg_replace( '#(.*plugins[^/]*/[^/]+)(/.*)?#', '$1', $file_name, 1, $count );
127 | if ( 0 === $count ) {
128 | throw new \Exception( "Class not located within a directory tree containing 'plugins': $file_name" );
129 | }
130 |
131 | // Make sure that we can reliably get the relative path inside of the content directory.
132 | $content_dir = realpath( trailingslashit( WP_CONTENT_DIR ) );
133 | if ( '/' !== \DIRECTORY_SEPARATOR ) {
134 | $content_dir = str_replace( \DIRECTORY_SEPARATOR, '/', $content_dir ); // Windows compat.
135 | }
136 | if ( 0 !== strpos( $plugin_dir, $content_dir ) ) {
137 | throw new \Exception( 'Plugin dir is not inside of WP_CONTENT_DIR' );
138 | }
139 | $content_sub_path = substr( $plugin_dir, strlen( $content_dir ) );
140 | $dir_url = content_url( trailingslashit( $content_sub_path ) );
141 | $dir_path = $plugin_dir;
142 | $dir_basename = basename( $plugin_dir );
143 | return compact( 'dir_url', 'dir_path', 'dir_basename' );
144 | }
145 |
146 | /**
147 | * Return whether we're on WordPress.com VIP production.
148 | *
149 | * @return bool
150 | */
151 | public function is_wpcom_vip_prod() {
152 | return ( defined( '\WPCOM_IS_VIP_ENV' ) && \WPCOM_IS_VIP_ENV );
153 | }
154 |
155 | /**
156 | * Call trigger_error() if not on VIP production.
157 | *
158 | * @param string $message Warning message.
159 | * @param int $code Warning code.
160 | */
161 | public function trigger_warning( $message, $code = \E_USER_WARNING ) {
162 | if ( ! $this->is_wpcom_vip_prod() ) {
163 | trigger_error( esc_html( get_class( $this ) . ': ' . $message ), $code );
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/php/class-plugin.php:
--------------------------------------------------------------------------------
1 |
73 |
74 |
The Customize Setting Validation plugin is obsolete. It was committed during the 4.6 release cycle in r37476. You can uninstall this plugin.
75 |
76 | doing_action( 'customize_validate_settings' )
106 | * in their sanitize callbacks to determine whether they should be strict
107 | * in how they sanitize the values, in other words to tell them whether they
108 | * can return null or a WP_Error object to mark the setting as invalid.
109 | *
110 | * This is in lieu of there being a 'customize_validate_{$setting_id}' filter,
111 | * or supplying an additional argument to 'customize_sanitize_{$setting_id}'
112 | * which would indicate that strict validation should be employed.
113 | *
114 | * @param \WP_Customize_Manager $wp_customize Manager instance.
115 | */
116 | public function do_customize_validate_settings( $wp_customize ) {
117 | do_action( 'customize_validate_settings', $wp_customize );
118 | }
119 |
120 | /**
121 | * Early at the customize_save action, iterate over all settings and check for any that are invalid.
122 | *
123 | * If any of the settings are invalid, short-circuit the WP_Customize_Manager::save() call with a
124 | * call to wp_send_json_error() sending back the invalid_settings.
125 | *
126 | * @access public
127 | * @action customize_save
128 | *
129 | * @param \WP_Customize_Manager $wp_customize Customizer manager.
130 | */
131 | public function validate_settings( \WP_Customize_Manager $wp_customize ) {
132 | global $wp_registered_widget_updates;
133 | $sanitized_value = null;
134 |
135 | /*
136 | * Check to see if any of the registered settings are invalid, and for
137 | * those that are invalid, build an array of the invalid messages.
138 | */
139 | $unsanitized_post_values = $wp_customize->unsanitized_post_values();
140 | foreach ( $unsanitized_post_values as $setting_id => $unsanitized_value ) {
141 | $sanitized_value = null;
142 | $setting = $wp_customize->get_setting( $setting_id );
143 | if ( ! $setting ) {
144 | continue;
145 | }
146 | if ( is_null( $unsanitized_value ) ) {
147 | continue;
148 | }
149 | $parsed_widget_id = $wp_customize->widgets->parse_widget_setting_id( $setting_id );
150 | $is_empty_widget_instance = (
151 | ! is_wp_error( $parsed_widget_id )
152 | &&
153 | is_array( $unsanitized_value )
154 | &&
155 | empty( $unsanitized_value )
156 | &&
157 | ! empty( $wp_customize->widgets )
158 | );
159 | if ( $is_empty_widget_instance ) {
160 | $instance = null;
161 | foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
162 | $is_wp_widget = (
163 | $name === $parsed_widget_id['id_base']
164 | &&
165 | is_callable( $control['callback'] )
166 | &&
167 | is_array( $control['callback'] )
168 | &&
169 | $control['callback'][0] instanceof \WP_Widget
170 | );
171 | if ( $is_wp_widget ) {
172 | // Note that error suppression is needed because a widget update() callback may have default values.
173 | // @todo All Core widgets should have proper defaults if the incoming array is empty.
174 | $instance = @call_user_func( array( $control['callback'][0], 'update' ), array(), array() );
175 | $sanitized_value = $wp_customize->widgets->sanitize_widget_js_instance( $instance );
176 | if ( ! is_null( $sanitized_value ) && ! is_wp_error( $sanitized_value ) ) {
177 | $wp_customize->set_post_value( $setting_id, $sanitized_value );
178 | }
179 | break;
180 | }
181 | }
182 | }
183 |
184 | if ( ! isset( $sanitized_value ) ) {
185 | $sanitized_value = $setting->sanitize( $unsanitized_value );
186 | }
187 | if ( is_null( $sanitized_value ) ) {
188 | $sanitized_value = new \WP_Error( 'invalid_value', __( 'Invalid value.', 'customize-setting-validation' ) );
189 | }
190 | if ( is_wp_error( $sanitized_value ) ) {
191 | $this->invalid_settings[ $setting_id ] = $sanitized_value->get_error_message();
192 | }
193 | }
194 |
195 | $invalid_count = count( $this->invalid_settings );
196 |
197 | // No invalid settings, do not short-circuit.
198 | if ( 0 === $invalid_count ) {
199 | return;
200 | }
201 |
202 | $response = array(
203 | 'message' => sprintf( _n( 'There is %d invalid setting.', 'There are %d invalid settings.', $invalid_count, 'customize-setting-validation' ), $invalid_count ),
204 | 'invalid_settings' => $this->invalid_settings,
205 | );
206 |
207 | /** This filter is documented in wp-includes/class-wp-customize-manager.php */
208 | $response = apply_filters( 'customize_save_response', $response, $wp_customize );
209 |
210 | /*
211 | * This assumes that the method is being called in the context of
212 | * WP_Customize_Manager::save(), which calls wp_json_send_success()
213 | * at the end.
214 | */
215 | wp_send_json_error( $response );
216 | }
217 |
218 | /**
219 | * Keep track of which settings were actually saved.
220 | *
221 | * Note that the footwork with id_bases is needed because there is no
222 | * action for customize_save_{$setting_id}.
223 | *
224 | * @access private
225 | * @param \WP_Customize_Manager $wp_customize Customize manager.
226 | * @action customize_save
227 | */
228 | public function _add_actions_for_flagging_saved_settings( \WP_Customize_Manager $wp_customize ) {
229 | $seen_id_bases = array();
230 | foreach ( $wp_customize->settings() as $setting ) {
231 | $id_data = $setting->id_data();
232 | if ( ! isset( $seen_id_bases[ $id_data['base'] ] ) ) {
233 | add_action( 'customize_save_' . $id_data['base'], array( $this, '_flag_saved_setting_value' ) );
234 | $seen_id_bases[ $id_data['base'] ] = true;
235 | }
236 | }
237 | }
238 |
239 | /**
240 | * Flag which settings were saved.
241 | *
242 | * @access private
243 | * @param \WP_Customize_Setting $setting Saved setting.
244 | * @see \WP_Customize_Setting::save()
245 | * @action customize_save
246 | */
247 | public function _flag_saved_setting_value( \WP_Customize_Setting $setting ) {
248 | $this->saved_setting_values[ $setting->id ] = null;
249 | }
250 |
251 | /**
252 | * Register any widgets that that get saved so that they will not get stripped
253 | * out when \WP_Customize_Widgets::sanitize_sidebar_widgets_js_instance() is called.
254 | *
255 | * @todo In Core, \WP_Customize_Widgets should register any widget that gets saved.
256 | *
257 | * @param \WP_Customize_Manager $wp_customize Customize manager.
258 | * @param array $saved_setting_ids Saved setting IDs.
259 | */
260 | public function register_widgets_for_saved_settings( $wp_customize, $saved_setting_ids ) {
261 | global $wp_registered_widgets;
262 |
263 | foreach ( $saved_setting_ids as $setting_id ) {
264 | $parsed_setting_id = $wp_customize->widgets->parse_widget_setting_id( $setting_id );
265 | if ( is_wp_error( $parsed_setting_id ) ) {
266 | continue;
267 | }
268 | $widget_id = $parsed_setting_id['id_base'];
269 | if ( $parsed_setting_id['number'] ) {
270 | $widget_id .= '-' . $parsed_setting_id['number'];
271 | }
272 |
273 | /*
274 | * For the purposes of \WP_Customize_Widgets::sanitize_sidebar_widgets_js_instance()
275 | * all we need to do is make sure that the array key exists for the given widget ID.
276 | */
277 | if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
278 | $wp_registered_widgets[ $widget_id ] = null;
279 | }
280 | }
281 | }
282 |
283 | /**
284 | * Gather the saved setting values.
285 | *
286 | * @param \WP_Customize_Manager $wp_customize Customizer manager.
287 | * @action customize_save_after
288 | */
289 | public function gather_saved_setting_values( \WP_Customize_Manager $wp_customize ) {
290 | $setting_ids = array_keys( $this->saved_setting_values );
291 | $this->register_widgets_for_saved_settings( $wp_customize, $setting_ids );
292 |
293 | foreach ( $setting_ids as $setting_id ) {
294 | $setting = $wp_customize->get_setting( $setting_id );
295 | if ( $setting ) {
296 | $this->saved_setting_values[ $setting_id ] = $setting->js_value();
297 | }
298 | }
299 | }
300 |
301 | /**
302 | * Export any invalid setting data to the Customizer JS client.
303 | *
304 | * @filter customize_save_response
305 | *
306 | * @param array $response Return value for customize-save Ajax request.
307 | * @return array Return value for customize-save Ajax request.
308 | */
309 | public function filter_customize_save_response( $response ) {
310 | if ( ! empty( $this->invalid_settings ) ) {
311 | $response['invalid_settings'] = $this->invalid_settings;
312 | }
313 | if ( ! empty( $this->saved_setting_values ) ) {
314 | $response['sanitized_setting_values'] = $this->saved_setting_values;
315 | }
316 | return $response;
317 | }
318 |
319 | /**
320 | * Print templates.
321 | */
322 | public function print_templates() {
323 | ?>
324 |
331 |
2 |
3 | Generally-applicable sniffs for WordPress plugins
4 |
5 |
6 |
7 |
8 |
9 | */dev-lib/*
10 | */node_modules/*
11 | */vendor/*
12 |
13 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 | dev-lib/phpunit-plugin.xml
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 | # Customize Setting Validation
3 |
4 | 
5 | [Obsolete] Core feature plugin for Customizer setting validation, error messaging, and transactional/atomic saves. See Trac #34893.
6 |
7 | **Contributors:** [westonruter](https://profiles.wordpress.org/westonruter), [xwp](https://profiles.wordpress.org/xwp)
8 | **Requires at least:** 4.4
9 | **Tested up to:** 4.6-alpha
10 | **Stable tag:** 0.1.3
11 | **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)
12 |
13 | [](https://travis-ci.org/xwp/wp-customize-setting-validation) [](https://coveralls.io/github/xwp/wp-customize-setting-validation) [](http://gruntjs.com) [](https://david-dm.org/xwp/wp-customize-setting-validation#info=devDependencies)
14 |
15 | ## Description ##
16 |
17 | Notice: The Customize Setting Validation plugin is obsolete. It was committed during the 4.6 release cycle in r37476.
18 |
19 | This feature plugin allows setting values to be validated and for any validation errors to block the Customizer from
20 | saving any setting until all are valid. Additionally, once a successful save is performed on the server, any settings
21 | that have resulting PHP-sanitized values which differ from the JS values will be updated on the client to match, while
22 | retaining the non-dirty saved state.
23 |
24 | The functionality here will be proposed for inclusion in WordPress Core via Trac [#34893](https://core.trac.wordpress.org/ticket/34893):
25 | Improve Customizer setting validation model.
26 |
27 | See demo video of “[Customize Validate Entitled Settings](https://gist.github.com/westonruter/1016332b18ee7946dec3)” plugin which forces the site title, widget titles, and nav menu item labels to all be populated and to start with an upper-case letter:
28 |
29 | [](https://www.youtube.com/watch?v=ZNk6FhtS8TM)
30 |
31 | Settings in the Customizer rely on sanitization to ensure that only valid values get persisted to the database. The sanitization in the Customizer generally allows values to be passed through to be persisted and does not enforce validation that blocks the saving of the value. This is in large part because there is no standard facility for showing error messages relating to Customizer controls, and there is no standard method to raise validation errors. A Customizer setting _can_ be blocked from being saved by returning `null` from the `WP_Customize_Setting::sanitize()` method (i.e. generally returned via `customize_sanitize_{$setting_id}`). When this is done, however, the modified value completely disappears from the preview with no indication for why the value seems to be reset to the default.
32 |
33 | In JavaScript there is the `wp.customize.Setting.prototype.validate()` method that can be extended to return `null` in the case where the value should be rejected, but again this does not provide a way to display a validation message: the user can be entering data but they just stop seeing the value making changes in the preview without any feedback. Even worse, if the JS `validate` method actually manipulates the value to make it valid, there can be strange behavior in the UI as the user provides input, likely having to do with the two-way data binding of `wp.customize.Element` instances.
34 |
35 | Furthermore, if one setting is blocked from being saved by means of validation in the sanitization method, the other settings in the Customizer state may very well be completely valid, and thus they would save successfully. The result is that some settings would get saved, whereas others would not, and the user wouldn't know which were successful and which failed (again, since there is no standard mechanism for showing validation error message). The Customizer state would only partially get persisted to the database. This isn't good.
36 |
37 | Lastly, once the settings are successfully saved, if any of the PHP-sanitization differs in any way from the JS-sanitization on the client, the difference in value will not be apparent in the Customizer controls.
38 |
39 | So this plugin aims to solve both these problems by:
40 |
41 | * Validating settings on server before save.
42 | * Displaying validation error messages from server and from JS client.
43 | * Performing transactional/atomic setting saving, rejecting all settings if one is invalid.
44 | * Sync back the PHP-sanitized saved setting values to the JS client and ensure controls are populated with the actual persisted values.
45 |
46 | Note that the transactional/atomic saving here in setting validation is not the same as the [Customizer Transactions proposal](https://make.wordpress.org/core/2015/01/26/customizer-transactions-proposal/), although the two are complimentary. A transaction should not be published/committed until all of its settings are valid.
47 |
48 | To do server-side validation of a setting, implement a setting sanitizer that returns `null` or a `WP_Error` object:
49 |
50 | ```php
51 | add_setting( 'year_established', array(
53 | 'type' => 'option',
54 | 'sanitize_callback' => function ( $value ) {
55 | $value = intval( $value );
56 |
57 | // Apply strict validation when the sanitize callback is called during.customize_validate_settings.
58 | if ( doing_action( 'customize_validate_settings' ) && ( $value < 1900 || $value > 2100 ) ) {
59 | return new WP_Error( 'invalid_value', __( 'Year must be between 1900 and 2100.' ) );
60 | }
61 |
62 | $value = min( 2100, max( $value, 1900 ) );
63 | return $value;
64 | }
65 | ) );
66 | ```
67 |
68 | The validation error message can also be set programmatically by JS by calling `setting.validationMessage.set()`,
69 | for example from an extended `setting.validate()` method. The `validationMessage` is inspired by HTML5.
70 |
71 | For a demonstration of the functionality made possible with this Customizer setting validation API,
72 | including how to do client-side validation, see the “[Customize_Validate_Entitled_Settings](https://gist.github.com/westonruter/1016332b18ee7946dec3)” plugin.
73 | It will validate that the Site Name (`blogname`), nav menu item titles, and widget titles are all fully populated.
74 | The validation is done both on the client and on the server.
75 |
76 | ## Screenshots ##
77 |
78 | ### Invalid site title.
79 |
80 | 
81 |
82 | ### Invalid widget title.
83 |
84 | 
85 |
86 | ### Invalid nav menu item title.
87 |
88 | 
89 |
90 | ## Changelog ##
91 |
92 | ### 0.1.3 ###
93 | Short-circuit and show admin notice if plugin is obsolete and can be uninstalled.
94 |
95 | ### 0.1.2 ###
96 | Prevent invalid value from being accepted due to logic error with a variable leaking into the next loop iteration.
97 |
98 | ### 0.1.1 ###
99 | * Handle relative `WP_CONTENT_DIR` in locate_plugin.
100 | * Add banner and icon for WP plugin directory.
101 | * Prevent validation message from sliding up/down repeatedly.
102 | * Reset validation message element height to auto after slideDown finishes.
103 | * Add missing wp-util dependency.
104 |
105 | ### 0.1 ###
106 | Initial release.
107 |
108 |
109 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === Customize Setting Validation ===
2 | Contributors: westonruter, xwp
3 | Requires at least: 4.4
4 | Tested up to: 4.6-alpha
5 | Stable tag: 0.1.3
6 | License: GPLv2 or later
7 | License URI: http://www.gnu.org/licenses/gpl-2.0.html
8 |
9 | [Obsolete] Core feature plugin for Customizer setting validation, error messaging, and transactional/atomic saves. See Trac #34893.
10 |
11 | == Description ==
12 |
13 | Notice: The Customize Setting Validation plugin is obsolete. It was committed during the 4.6 release cycle in r37476.
14 |
15 | This feature plugin allows setting values to be validated and for any validation errors to block the Customizer from
16 | saving any setting until all are valid. Additionally, once a successful save is performed on the server, any settings
17 | that have resulting PHP-sanitized values which differ from the JS values will be updated on the client to match, while
18 | retaining the non-dirty saved state.
19 |
20 | The functionality here will be proposed for inclusion in WordPress Core via Trac [#34893](https://core.trac.wordpress.org/ticket/34893):
21 | Improve Customizer setting validation model.
22 |
23 | See demo video of “[Customize Validate Entitled Settings](https://gist.github.com/westonruter/1016332b18ee7946dec3)” plugin which forces the site title, widget titles, and nav menu item labels to all be populated and to start with an upper-case letter:
24 |
25 | [youtube https://www.youtube.com/watch?v=ZNk6FhtS8TM]
26 |
27 | Settings in the Customizer rely on sanitization to ensure that only valid values get persisted to the database. The sanitization in the Customizer generally allows values to be passed through to be persisted and does not enforce validation that blocks the saving of the value. This is in large part because there is no standard facility for showing error messages relating to Customizer controls, and there is no standard method to raise validation errors. A Customizer setting _can_ be blocked from being saved by returning `null` from the `WP_Customize_Setting::sanitize()` method (i.e. generally returned via `customize_sanitize_{$setting_id}`). When this is done, however, the modified value completely disappears from the preview with no indication for why the value seems to be reset to the default.
28 |
29 | In JavaScript there is the `wp.customize.Setting.prototype.validate()` method that can be extended to return `null` in the case where the value should be rejected, but again this does not provide a way to display a validation message: the user can be entering data but they just stop seeing the value making changes in the preview without any feedback. Even worse, if the JS `validate` method actually manipulates the value to make it valid, there can be strange behavior in the UI as the user provides input, likely having to do with the two-way data binding of `wp.customize.Element` instances.
30 |
31 | Furthermore, if one setting is blocked from being saved by means of validation in the sanitization method, the other settings in the Customizer state may very well be completely valid, and thus they would save successfully. The result is that some settings would get saved, whereas others would not, and the user wouldn't know which were successful and which failed (again, since there is no standard mechanism for showing validation error message). The Customizer state would only partially get persisted to the database. This isn't good.
32 |
33 | Lastly, once the settings are successfully saved, if any of the PHP-sanitization differs in any way from the JS-sanitization on the client, the difference in value will not be apparent in the Customizer controls.
34 |
35 | So this plugin aims to solve both these problems by:
36 |
37 | * Validating settings on server before save.
38 | * Displaying validation error messages from server and from JS client.
39 | * Performing transactional/atomic setting saving, rejecting all settings if one is invalid.
40 | * Sync back the PHP-sanitized saved setting values to the JS client and ensure controls are populated with the actual persisted values.
41 |
42 | Note that the transactional/atomic saving here in setting validation is not the same as the [Customizer Transactions proposal](https://make.wordpress.org/core/2015/01/26/customizer-transactions-proposal/), although the two are complimentary. A transaction should not be published/committed until all of its settings are valid.
43 |
44 | To do server-side validation of a setting, implement a setting sanitizer that returns `null` or a `WP_Error` object:
45 |
46 |
47 | add_setting( 'year_established', array(
49 | 'type' => 'option',
50 | 'sanitize_callback' => function ( $value ) {
51 | $value = intval( $value );
52 |
53 | // Apply strict validation when the sanitize callback is called during.customize_validate_settings.
54 | if ( doing_action( 'customize_validate_settings' ) && ( $value < 1900 || $value > 2100 ) ) {
55 | return new WP_Error( 'invalid_value', __( 'Year must be between 1900 and 2100.' ) );
56 | }
57 |
58 | $value = min( 2100, max( $value, 1900 ) );
59 | return $value;
60 | }
61 | ) );
62 |
63 |
64 | The validation error message can also be set programmatically by JS by calling `setting.validationMessage.set()`,
65 | for example from an extended `setting.validate()` method. The `validationMessage` is inspired by HTML5.
66 |
67 | For a demonstration of the functionality made possible with this Customizer setting validation API,
68 | including how to do client-side validation, see the “[Customize_Validate_Entitled_Settings](https://gist.github.com/westonruter/1016332b18ee7946dec3)” plugin.
69 | It will validate that the Site Name (`blogname`), nav menu item titles, and widget titles are all fully populated.
70 | The validation is done both on the client and on the server.
71 |
72 | == Screenshots ==
73 |
74 | 1. Invalid site title.
75 | 2. Invalid widget title.
76 | 3. Invalid nav menu item title.
77 |
78 | == Changelog ==
79 |
80 | = 0.1.3 =
81 | Short-circuit and show admin notice if plugin is obsolete and can be uninstalled.
82 |
83 | = 0.1.2 =
84 | Prevent invalid value from being accepted due to logic error with a variable leaking into the next loop iteration.
85 |
86 | = 0.1.1 =
87 |
88 | * Handle relative `WP_CONTENT_DIR` in locate_plugin.
89 | * Add banner and icon for WP plugin directory.
90 | * Prevent validation message from sliding up/down repeatedly.
91 | * Reset validation message element height to auto after slideDown finishes.
92 | * Add missing wp-util dependency.
93 |
94 | = 0.1 =
95 |
96 | Initial release.
97 |
--------------------------------------------------------------------------------
/tests/php/test-class-plugin-base.php:
--------------------------------------------------------------------------------
1 | plugin = get_plugin_instance();
32 | }
33 |
34 | /**
35 | * Test locate_plugin.
36 | *
37 | * @see Plugin_Base::locate_plugin()
38 | */
39 | public function test_locate_plugin() {
40 | $location = $this->plugin->locate_plugin();
41 | $this->assertEquals( 'customize-setting-validation', $location['dir_basename'] );
42 | $this->assertContains( 'plugins/customize-setting-validation', $location['dir_path'] );
43 | $this->assertContains( 'plugins/customize-setting-validation', $location['dir_url'] );
44 | }
45 |
46 | /**
47 | * Tests for trigger_warning().
48 | *
49 | * @see Plugin_Base::trigger_warning()
50 | */
51 | public function test_trigger_warning() {
52 | $obj = $this;
53 | set_error_handler( function ( $errno, $errstr ) use ( $obj ) {
54 | $obj->assertEquals( 'CustomizeSettingValidation\Plugin: Param is 0!', $errstr );
55 | $obj->assertEquals( \E_USER_WARNING, $errno );
56 | } );
57 | $this->plugin->trigger_warning( 'Param is 0!', \E_USER_WARNING );
58 | restore_error_handler();
59 | }
60 |
61 | /**
62 | * Test is_wpcom_vip_prod().
63 | *
64 | * @see Plugin_Base::is_wpcom_vip_prod()
65 | */
66 | public function test_is_wpcom_vip_prod() {
67 | if ( ! defined( 'WPCOM_IS_VIP_ENV' ) ) {
68 | $this->assertFalse( $this->plugin->is_wpcom_vip_prod() );
69 | define( 'WPCOM_IS_VIP_ENV', true );
70 | }
71 | $this->assertEquals( WPCOM_IS_VIP_ENV, $this->plugin->is_wpcom_vip_prod() );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/php/test-class-plugin.php:
--------------------------------------------------------------------------------
1 | assertEquals( 10, has_action( 'after_setup_theme', array( $plugin, 'init' ) ) );
25 | }
26 |
27 | /**
28 | * Test for init() method.
29 | *
30 | * @see Plugin::init()
31 | */
32 | public function test_init() {
33 | $this->markTestIncomplete();
34 | }
35 |
36 | /* Put other test functions here... */
37 | }
38 |
--------------------------------------------------------------------------------
/tests/test-customize-setting-validation.php:
--------------------------------------------------------------------------------
1 | assertContains( '