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/tests/phpunit/tests/customize/manager.php

    r37040 r37476  
    124124        $bar_setting = $manager->get_setting( 'bar' );
    125125        $this->assertEquals( 'post_value_bar_default', $manager->post_value( $bar_setting, 'post_value_bar_default' ), 'Expected post_value($bar_setting, $default) to return $default since no value supplied in $_POST[customized][bar]' );
     126    }
     127
     128    /**
     129     * Test the WP_Customize_Manager::post_value() method for a setting value that fails validation.
     130     *
     131     * @ticket 34893
     132     */
     133    function test_invalid_post_value() {
     134        $default_value = 'foo_default';
     135        $setting = $this->manager->add_setting( 'foo', array(
     136            'validate_callback' => array( $this, 'filter_customize_validate_foo' ),
     137            'sanitize_callback' => array( $this, 'filter_customize_sanitize_foo' ),
     138        ) );
     139        $this->assertEquals( $default_value, $this->manager->post_value( $setting, $default_value ) );
     140        $this->assertEquals( $default_value, $setting->post_value( $default_value ) );
     141
     142        $post_value = 'bar';
     143        $this->manager->set_post_value( 'foo', $post_value );
     144        $this->assertEquals( strtoupper( $post_value ), $this->manager->post_value( $setting, $default_value ) );
     145        $this->assertEquals( strtoupper( $post_value ), $setting->post_value( $default_value ) );
     146
     147        $this->manager->set_post_value( 'foo', 'return_wp_error_in_sanitize' );
     148        $this->assertEquals( $default_value, $this->manager->post_value( $setting, $default_value ) );
     149        $this->assertEquals( $default_value, $setting->post_value( $default_value ) );
     150
     151        $this->manager->set_post_value( 'foo', 'return_null_in_sanitize' );
     152        $this->assertEquals( $default_value, $this->manager->post_value( $setting, $default_value ) );
     153        $this->assertEquals( $default_value, $setting->post_value( $default_value ) );
     154
     155        $post_value = '<script>evil</script>';
     156        $this->manager->set_post_value( 'foo', $post_value );
     157        $this->assertEquals( $default_value, $this->manager->post_value( $setting, $default_value ) );
     158        $this->assertEquals( $default_value, $setting->post_value( $default_value ) );
     159    }
     160
     161    /**
     162     * Filter customize_validate callback.
     163     *
     164     * @param mixed $value Value.
     165     * @return string|WP_Error
     166     */
     167    function filter_customize_sanitize_foo( $value ) {
     168        if ( 'return_null_in_sanitize' === $value ) {
     169            $value = null;
     170        } elseif ( is_string( $value ) ) {
     171            $value = strtoupper( $value );
     172            if ( false !== stripos( $value, 'return_wp_error_in_sanitize' ) ) {
     173                $value = new WP_Error( 'invalid_value_in_sanitize', __( 'Invalid value.' ), array( 'source' => 'filter_customize_sanitize_foo' ) );
     174            }
     175        }
     176        return $value;
     177    }
     178
     179    /**
     180     * Filter customize_validate callback.
     181     *
     182     * @param WP_Error $validity Validity.
     183     * @param mixed    $value    Value.
     184     * @return WP_Error
     185     */
     186    function filter_customize_validate_foo( $validity, $value ) {
     187        if ( false !== stripos( $value, '<script' ) ) {
     188            $validity->add( 'invalid_value_in_validate', __( 'Invalid value.' ), array( 'source' => 'filter_customize_validate_foo' ) );
     189        }
     190        return $validity;
     191    }
     192
     193    /**
     194     * Test WP_Customize_Manager::validate_setting_values().
     195     *
     196     * @see WP_Customize_Manager::validate_setting_values()
     197     */
     198    function test_validate_setting_values() {
     199        $default_value = 'foo_default';
     200        $setting = $this->manager->add_setting( 'foo', array(
     201            'validate_callback' => array( $this, 'filter_customize_validate_foo' ),
     202            'sanitize_callback' => array( $this, 'filter_customize_sanitize_foo' ),
     203        ) );
     204
     205        $post_value = 'bar';
     206        $this->manager->set_post_value( 'foo', $post_value );
     207        $this->assertEmpty( $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() ) );
     208
     209        $this->manager->set_post_value( 'foo', 'return_wp_error_in_sanitize' );
     210        $invalid_settings = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
     211        $this->assertCount( 1, $invalid_settings );
     212        $this->assertArrayHasKey( $setting->id, $invalid_settings );
     213        $this->assertInstanceOf( 'WP_Error', $invalid_settings[ $setting->id ] );
     214        $error = $invalid_settings[ $setting->id ];
     215        $this->assertEquals( 'invalid_value_in_sanitize', $error->get_error_code() );
     216        $this->assertEquals( array( 'source' => 'filter_customize_sanitize_foo' ), $error->get_error_data() );
     217
     218        $this->manager->set_post_value( 'foo', 'return_null_in_sanitize' );
     219        $invalid_settings = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
     220        $this->assertCount( 1, $invalid_settings );
     221        $this->assertArrayHasKey( $setting->id, $invalid_settings );
     222        $this->assertInstanceOf( 'WP_Error', $invalid_settings[ $setting->id ] );
     223        $this->assertNull( $invalid_settings[ $setting->id ]->get_error_data() );
     224
     225        $post_value = '<script>evil</script>';
     226        $this->manager->set_post_value( 'foo', $post_value );
     227        $invalid_settings = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
     228        $this->assertCount( 1, $invalid_settings );
     229        $this->assertArrayHasKey( $setting->id, $invalid_settings );
     230        $this->assertInstanceOf( 'WP_Error', $invalid_settings[ $setting->id ] );
     231        $error = $invalid_settings[ $setting->id ];
     232        $this->assertEquals( 'invalid_value_in_validate', $error->get_error_code() );
     233        $this->assertEquals( array( 'source' => 'filter_customize_validate_foo' ), $error->get_error_data() );
    126234    }
    127235
     
    417525        $this->assertContains( 'var _wpCustomizeSettings =', $content );
    418526        $this->assertContains( '"blogname"', $content );
     527        $this->assertContains( '"type":"option"', $content );
    419528        $this->assertContains( '_wpCustomizeSettings.controls', $content );
    420529        $this->assertContains( '_wpCustomizeSettings.settings', $content );
Note: See TracChangeset for help on using the changeset viewer.