diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index 270770dab5..e1021a3e2e 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -1521,18 +1521,25 @@ settings = $.map( control.params.settings, function( value ) { return value; }); - api.apply( api, settings.concat( function () { - var key; + if ( 0 === settings.length ) { + control.setting = null; control.settings = {}; - for ( key in control.params.settings ) { - control.settings[ key ] = api( control.params.settings[ key ] ); - } - - control.setting = control.settings['default'] || null; - control.embed(); - }) ); + } else { + api.apply( api, settings.concat( function() { + var key; + + control.settings = {}; + for ( key in control.params.settings ) { + control.settings[ key ] = api( control.params.settings[ key ] ); + } + + control.setting = control.settings['default'] || null; + + control.embed(); + }) ); + } // After the control is embedded on the page, invoke the "ready" method. control.deferred.embedded.done( function () { diff --git a/src/wp-includes/class-wp-customize-control.php b/src/wp-includes/class-wp-customize-control.php index a892b10413..52a8a91ee6 100644 --- a/src/wp-includes/class-wp-customize-control.php +++ b/src/wp-includes/class-wp-customize-control.php @@ -64,6 +64,18 @@ class WP_Customize_Control { */ public $setting = 'default'; + /** + * Capability required to use this control. + * + * Normally this is empty and the capability is derived from the capabilities + * of the associated `$settings`. + * + * @since 4.5.0 + * @access public + * @var string + */ + public $capability; + /** * @access public * @var int @@ -187,7 +199,7 @@ class WP_Customize_Control { $this->instance_number = self::$instance_count; // Process settings. - if ( empty( $this->settings ) ) { + if ( ! isset( $this->settings ) ) { $this->settings = $id; } @@ -196,7 +208,7 @@ class WP_Customize_Control { foreach ( $this->settings as $key => $setting ) { $settings[ $key ] = $this->manager->get_setting( $setting ); } - } else { + } else if ( is_string( $this->settings ) ) { $this->setting = $this->manager->get_setting( $this->settings ); $settings['default'] = $this->setting; } @@ -299,21 +311,32 @@ class WP_Customize_Control { } /** - * Check if the theme supports the control and check user capabilities. + * Checks if the user can use this control. + * + * Returns false if the user cannot manipulate one of the associated settings, + * or if one of the associated settings does not exist. Also returns false if + * the associated section does not exist or if its capability check returns + * false. * * @since 3.4.0 * * @return bool False if theme doesn't support the control or user doesn't have the required permissions, otherwise true. */ final public function check_capabilities() { + if ( ! empty( $this->capability ) && ! current_user_can( $this->capability ) ) { + return false; + } + foreach ( $this->settings as $setting ) { - if ( ! $setting->check_capabilities() ) + if ( ! $setting || ! $setting->check_capabilities() ) { return false; + } } $section = $this->manager->get_section( $this->section ); - if ( isset( $section ) && ! $section->check_capabilities() ) + if ( isset( $section ) && ! $section->check_capabilities() ) { return false; + } return true; } diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index 627f57c670..16ef385259 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -606,28 +606,20 @@ final class WP_Customize_Nav_Menus { 'priority' => 999, ) ) ); - $this->manager->add_setting( 'new_menu_name', array( - 'type' => 'new_menu', - 'default' => '', - 'transport' => isset( $this->manager->selective_refresh ) ? 'postMessage' : 'refresh', - ) ); - $this->manager->add_control( 'new_menu_name', array( 'label' => '', 'section' => 'add_menu', 'type' => 'text', + 'settings' => array(), 'input_attrs' => array( 'class' => 'menu-name-field', 'placeholder' => __( 'New menu name' ), ), ) ); - $this->manager->add_setting( 'create_new_menu', array( - 'type' => 'new_menu', - ) ); - $this->manager->add_control( new WP_Customize_New_Menu_Control( $this->manager, 'create_new_menu', array( - 'section' => 'add_menu', + 'section' => 'add_menu', + 'settings' => array(), ) ) ); } @@ -851,6 +843,8 @@ final class WP_Customize_Nav_Menus { 'type' => 'nav_menu_instance', 'render_callback' => array( $this, 'render_nav_menu_partial' ), 'container_inclusive' => true, + 'settings' => array(), // Empty because the nav menu instance may relate to a menu or a location. + 'capability' => 'edit_theme_options', ) ); } diff --git a/src/wp-includes/class-wp-customize-widgets.php b/src/wp-includes/class-wp-customize-widgets.php index f65e7bfe48..69cfcc1854 100644 --- a/src/wp-includes/class-wp-customize-widgets.php +++ b/src/wp-includes/class-wp-customize-widgets.php @@ -1485,16 +1485,18 @@ final class WP_Customize_Widgets { */ public function customize_dynamic_partial_args( $partial_args, $partial_id ) { - if ( preg_match( '/^widget\[.+\]$/', $partial_id ) ) { + if ( preg_match( '/^widget\[(?P.+)\]$/', $partial_id, $matches ) ) { if ( false === $partial_args ) { $partial_args = array(); } $partial_args = array_merge( $partial_args, array( - 'type' => 'widget', - 'render_callback' => array( $this, 'render_widget_partial' ), + 'type' => 'widget', + 'render_callback' => array( $this, 'render_widget_partial' ), 'container_inclusive' => true, + 'settings' => array( $this->get_setting_id( $matches['widget_id'] ) ), + 'capability' => 'edit_theme_options', ) ); } diff --git a/src/wp-includes/customize/class-wp-customize-partial.php b/src/wp-includes/customize/class-wp-customize-partial.php index 3cb410b078..c27b3f058c 100644 --- a/src/wp-includes/customize/class-wp-customize-partial.php +++ b/src/wp-includes/customize/class-wp-customize-partial.php @@ -89,6 +89,18 @@ class WP_Customize_Partial { */ public $primary_setting; + /** + * Capability required to edit this partial. + * + * Normally this is empty and the capability is derived from the capabilities + * of the associated `$settings`. + * + * @since 4.5.0 + * @access public + * @var string + */ + public $capability; + /** * Render callback. * @@ -157,7 +169,7 @@ class WP_Customize_Partial { } // Process settings. - if ( empty( $this->settings ) ) { + if ( ! isset( $this->settings ) ) { $this->settings = array( $id ); } else if ( is_string( $this->settings ) ) { $this->settings = array( $this->settings ); @@ -299,6 +311,9 @@ class WP_Customize_Partial { * or if one of the associated settings does not exist. */ final public function check_capabilities() { + if ( ! empty( $this->capability ) && ! current_user_can( $this->capability ) ) { + return false; + } foreach ( $this->settings as $setting_id ) { $setting = $this->component->manager->get_setting( $setting_id ); if ( ! $setting || ! $setting->check_capabilities() ) { diff --git a/tests/phpunit/tests/customize/control.php b/tests/phpunit/tests/customize/control.php new file mode 100644 index 0000000000..e6d1141219 --- /dev/null +++ b/tests/phpunit/tests/customize/control.php @@ -0,0 +1,88 @@ +wp_customize = $GLOBALS['wp_customize']; + } + + /** + * Test WP_Customize_Control::check_capabilities(). + * + * @see WP_Customize_Control::check_capabilities() + */ + function test_check_capabilities() { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + do_action( 'customize_register', $this->wp_customize ); + $control = new WP_Customize_Control( $this->wp_customize, 'blogname', array( + 'settings' => array( 'blogname' ), + ) ); + $this->assertTrue( $control->check_capabilities() ); + + $control = new WP_Customize_Control( $this->wp_customize, 'blogname', array( + 'settings' => array( 'blogname', 'non_existing' ), + ) ); + $this->assertFalse( $control->check_capabilities() ); + + $this->wp_customize->add_setting( 'top_secret_message', array( + 'capability' => 'top_secret_clearance', + ) ); + $control = new WP_Customize_Control( $this->wp_customize, 'blogname', array( + 'settings' => array( 'blogname', 'top_secret_clearance' ), + ) ); + $this->assertFalse( $control->check_capabilities() ); + + $control = new WP_Customize_Control( $this->wp_customize, 'no_setting', array( + 'settings' => array(), + ) ); + $this->assertTrue( $control->check_capabilities() ); + + $control = new WP_Customize_Control( $this->wp_customize, 'no_setting', array( + 'settings' => array(), + 'capability' => 'top_secret_clearance', + ) ); + $this->assertFalse( $control->check_capabilities() ); + + $control = new WP_Customize_Control( $this->wp_customize, 'no_setting', array( + 'settings' => array(), + 'capability' => 'edit_theme_options', + ) ); + $this->assertTrue( $control->check_capabilities() ); + } + + /** + * Tear down. + */ + function tearDown() { + $this->wp_customize = null; + unset( $GLOBALS['wp_customize'] ); + parent::tearDown(); + } +} diff --git a/tests/phpunit/tests/customize/partial.php b/tests/phpunit/tests/customize/partial.php index 9b08fc1230..66cdfdb90f 100644 --- a/tests/phpunit/tests/customize/partial.php +++ b/tests/phpunit/tests/customize/partial.php @@ -325,6 +325,23 @@ class Test_WP_Customize_Partial extends WP_UnitTestCase { 'settings' => array( 'blogname', 'top_secret_clearance' ), ) ); $this->assertFalse( $partial->check_capabilities() ); + + $partial = new WP_Customize_Partial( $this->selective_refresh, 'no_setting', array( + 'settings' => array(), + ) ); + $this->assertTrue( $partial->check_capabilities() ); + + $partial = new WP_Customize_Partial( $this->selective_refresh, 'no_setting', array( + 'settings' => array(), + 'capability' => 'top_secret_clearance', + ) ); + $this->assertFalse( $partial->check_capabilities() ); + + $partial = new WP_Customize_Partial( $this->selective_refresh, 'no_setting', array( + 'settings' => array(), + 'capability' => 'edit_theme_options', + ) ); + $this->assertTrue( $partial->check_capabilities() ); } /** diff --git a/tests/qunit/fixtures/customize-menus.js b/tests/qunit/fixtures/customize-menus.js index 5fdd892f1f..f3ac0a1b0a 100755 --- a/tests/qunit/fixtures/customize-menus.js +++ b/tests/qunit/fixtures/customize-menus.js @@ -394,11 +394,6 @@ window._wpCustomizeSettings.controls.new_menu_name = { 'description': '', 'instanceNumber': 46 }; -window._wpCustomizeSettings.settings.new_menu_name = { - 'value': '', - 'transport': 'postMessage', - 'dirty': false -}; // From nav-menu.js window.wpNavMenu = { diff --git a/tests/qunit/wp-admin/js/customize-controls.js b/tests/qunit/wp-admin/js/customize-controls.js index b274995097..47d695cbaf 100644 --- a/tests/qunit/wp-admin/js/customize-controls.js +++ b/tests/qunit/wp-admin/js/customize-controls.js @@ -100,6 +100,20 @@ jQuery( window ).load( function (){ equal( control.section(), 'fixture-section' ); } ); + module( 'Customizer control without associated settings' ); + test( 'Control can be created without settings', function() { + var control = new wp.customize.Control( 'settingless', { + params: { + content: jQuery( '
  • Hello World
  • ' ), + section: 'fixture-section' + } + } ); + wp.customize.control.add( control.id, control ); + equal( control.deferred.embedded.state(), 'resolved' ); + ok( null === control.setting ); + ok( jQuery.isEmptyObject( control.settings ) ); + } ); + // Begin sections. module( 'Customizer Section in Fixture' ); test( 'Fixture section exists', function () {