From 29629ecc52275dbe33cab514d25f5382aa1ba81e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Nov 2016 09:02:38 +0000 Subject: [PATCH] Customize: Allow starter content to apply in a new theme when switching from another theme containing changes. * Ensure that starter content can apply from theme B after previewing starter content in theme A. * Introduce new `starter_content` flag in changeset setting params which is used to capture whether a value is starter content and thus can be overridden. * Create changeset up-front with `starter_content` flags instead of waiting for `AUTOSAVE_INTERVAL`. * Eliminate instantiation of settings for widget instances in favor of directly calling `sanitize_widget_js_instance`. This eliminates issues with looking for widgets before they are registered. * Ensure that non-placeholders (inline arrays instead of string references) can be supplied in starter content. * Re-use auto-draft posts as starter content across theme switches. * Introduce `starter_content` param for `WP_Customize_Manager::save_changeset_post()` which is `false` except when starter content is being loaded on a `fresh_site`. See #38114. Fixes #38541. git-svn-id: https://develop.svn.wordpress.org/trunk@39241 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-customize-manager.php | 194 +++++++++++++++--- .../class-wp-customize-nav-menus.php | 1 + src/wp-includes/theme.php | 59 +++--- 3 files changed, 198 insertions(+), 56 deletions(-) diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index 1a9c1c5523..3aa7ebcf90 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -523,8 +523,12 @@ final class WP_Customize_Manager { } } - // Import theme starter content for fresh installs when landing in the customizer and no existing changeset loaded. - if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow && ! $this->changeset_post_id() ) { + /* + * Import theme starter content for fresh installs when landing in the customizer. + * Import starter content at after_setup_theme:100 so that any + * add_theme_support( 'starter-content' ) calls will have been made. + */ + if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) { add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 ); } @@ -885,7 +889,16 @@ final class WP_Customize_Manager { } /** - * Import theme starter content into post values. + * Starter content setting IDs. + * + * @since 4.7.0 + * @access private + * @var array + */ + protected $starter_content_settings_ids = array(); + + /** + * Import theme starter content into the customized state. * * @since 4.7.0 * @access public @@ -897,6 +910,11 @@ final class WP_Customize_Manager { $starter_content = get_theme_starter_content(); } + $changeset_data = array(); + if ( $this->changeset_post_id() ) { + $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() ); + } + $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array(); $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array(); $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array(); @@ -932,45 +950,113 @@ final class WP_Customize_Manager { $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] ); $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] ); - $class = 'WP_Customize_Setting'; - - /** This filter is documented in wp-includes/class-wp-customize-manager.php */ - $args = apply_filters( 'customize_dynamic_setting_args', false, $setting_id ); - - if ( false !== $args ) { - - /** This filter is documented in wp-includes/class-wp-customize-manager.php */ - $class = apply_filters( 'customize_dynamic_setting_class', $class, $setting_id, $args ); - - $setting = new $class( $this, $setting_id, $args ); - $setting_value = call_user_func( $setting->sanitize_js_callback, $instance, $setting ); + $setting_value = $this->widgets->sanitize_widget_js_instance( $instance ); + if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { $this->set_post_value( $setting_id, $setting_value ); - $sidebar_widget_ids[] = $widget_id; + $this->starter_content_settings_ids[] = $setting_id; } + $sidebar_widget_ids[] = $widget_id; } - $this->set_post_value( sprintf( 'sidebars_widgets[%s]', $sidebar_id ), $sidebar_widget_ids ); + $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id ); + if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { + $this->set_post_value( $setting_id, $sidebar_widget_ids ); + $this->starter_content_settings_ids[] = $setting_id; + } } // Posts & pages. if ( ! empty( $posts ) ) { + $nav_menus_created_posts = array(); + if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) { + $nav_menus_created_posts = $changeset_data['nav_menus_created_posts']['value']; + } + + $existing_posts = array(); + if ( ! empty( $nav_menus_created_posts ) ) { + $existing_posts_query = new WP_Query( array( + 'post__in' => $nav_menus_created_posts, + 'post_status' => 'auto-draft', + 'post_type' => 'any', + 'number' => -1, + ) ); + foreach ( $existing_posts_query->posts as $existing_post ) { + $existing_posts[ $existing_post->post_type . ':' . $existing_post->post_name ] = $existing_post; + } + } + foreach ( array_keys( $posts ) as $post_symbol ) { + if ( empty( $posts[ $post_symbol ]['post_type'] ) ) { + continue; + } + $post_type = $posts[ $post_symbol ]['post_type']; + if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) { + $post_name = $posts[ $post_symbol ]['post_name']; + } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) { + $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] ); + } else { + continue; + } + + // Use existing auto-draft post if one already exists with the same type and name. + if ( isset( $existing_posts[ $post_type . ':' . $post_name ] ) ) { + $posts[ $post_symbol ]['ID'] = $existing_posts[ $post_type . ':' . $post_name ]->ID; + continue; + } + $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] ); if ( $r instanceof WP_Post ) { $posts[ $post_symbol ]['ID'] = $r->ID; } } - $this->set_post_value( 'nav_menus_created_posts', wp_list_pluck( $posts, 'ID' ) ); // This is why nav_menus component is dependency for adding posts. + + // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts. + $setting_id = 'nav_menus_created_posts'; + if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { + $nav_menus_created_posts = array_unique( array_merge( $nav_menus_created_posts, wp_list_pluck( $posts, 'ID' ) ) ); + $this->set_post_value( $setting_id, array_values( $nav_menus_created_posts ) ); + $this->starter_content_settings_ids[] = $setting_id; + } } // Nav menus. $placeholder_id = -1; + $reused_nav_menu_setting_ids = array(); foreach ( $nav_menus as $nav_menu_location => $nav_menu ) { - $nav_menu_term_id = $placeholder_id--; - $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $nav_menu_term_id ); + + $nav_menu_term_id = null; + $nav_menu_setting_id = null; + $matches = array(); + + // Look for an existing placeholder menu with starter content to re-use. + foreach ( $changeset_data as $setting_id => $setting_params ) { + $can_reuse = ( + ! empty( $setting_params['starter_content'] ) + && + ! in_array( $setting_id, $reused_nav_menu_setting_ids, true ) + && + preg_match( '#^nav_menu\[(?P-?\d+)\]$#', $setting_id, $matches ) + ); + if ( $can_reuse ) { + $nav_menu_term_id = intval( $matches['nav_menu_id'] ); + $nav_menu_setting_id = $setting_id; + $reused_nav_menu_setting_ids[] = $setting_id; + break; + } + } + + if ( ! $nav_menu_term_id ) { + while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) { + $placeholder_id--; + } + $nav_menu_term_id = $placeholder_id; + $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id ); + } + $this->set_post_value( $nav_menu_setting_id, array( 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location, ) ); + $this->starter_content_settings_ids[] = $nav_menu_setting_id; // @todo Add support for menu_item_parent. $position = 0; @@ -994,10 +1080,18 @@ final class WP_Customize_Manager { } else { $nav_menu_item['object_id'] = 0; } - $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item ); + + if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) { + $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item ); + $this->starter_content_settings_ids[] = $nav_menu_item_setting_id; + } } - $this->set_post_value( sprintf( 'nav_menu_locations[%s]', $nav_menu_location ), $nav_menu_term_id ); + $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location ); + if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { + $this->set_post_value( $setting_id, $nav_menu_term_id ); + $this->starter_content_settings_ids[] = $setting_id; + } } // Options. @@ -1005,7 +1099,11 @@ final class WP_Customize_Manager { if ( preg_match( '/^{{(?P.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) { $value = $posts[ $matches['symbol'] ]['ID']; } - $this->set_post_value( $name, $value ); + + if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) { + $this->set_post_value( $name, $value ); + $this->starter_content_settings_ids[] = $name; + } } // Theme mods. @@ -1013,8 +1111,38 @@ final class WP_Customize_Manager { if ( preg_match( '/^{{(?P.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) { $value = $posts[ $matches['symbol'] ]['ID']; } - $this->set_post_value( $name, $value ); + + if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) { + $this->set_post_value( $name, $value ); + $this->starter_content_settings_ids[] = $name; + } } + + if ( ! empty( $this->starter_content_settings_ids ) ) { + if ( did_action( 'customize_register' ) ) { + $this->_save_starter_content_changeset(); + } else { + add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 ); + } + } + } + + /** + * Save starter content changeset. + * + * @since 4.7.0 + * @access private + */ + public function _save_starter_content_changeset() { + + if ( empty( $this->starter_content_settings_ids ) ) { + return; + } + + $this->save_changeset_post( array( + 'data' => array_fill_keys( $this->starter_content_settings_ids, array( 'starter_content' => true ) ), + 'starter_content' => true, + ) ); } /** @@ -1823,11 +1951,12 @@ final class WP_Customize_Manager { * @param array $args { * Args for changeset post. * - * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values. - * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed. - * @type string $title Post title. Optional. - * @type string $date_gmt Date in GMT. Optional. - * @type int $user_id ID for user who is saving the changeset. Optional, defaults to the current user ID. + * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values. + * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed. + * @type string $title Post title. Optional. + * @type string $date_gmt Date in GMT. Optional. + * @type int $user_id ID for user who is saving the changeset. Optional, defaults to the current user ID. + * @type bool $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved. * } * * @return array|WP_Error Returns array on success and WP_Error with array data on error. @@ -1841,6 +1970,7 @@ final class WP_Customize_Manager { 'data' => array(), 'date_gmt' => null, 'user_id' => get_current_user_id(), + 'starter_content' => false, ), $args ); @@ -1977,6 +2107,7 @@ final class WP_Customize_Manager { if ( ! isset( $data[ $changeset_setting_id ] ) ) { $data[ $changeset_setting_id ] = array(); } + $data[ $changeset_setting_id ] = array_merge( $data[ $changeset_setting_id ], $setting_params, @@ -1985,6 +2116,11 @@ final class WP_Customize_Manager { 'user_id' => $args['user_id'], ) ); + + // Clear starter_content flag in data if changeset is not explicitly being updated for starter content. + if ( empty( $args['starter_content'] ) ) { + unset( $data[ $changeset_setting_id ]['starter_content'] ); + } } } diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index 37a8de8cb6..7ce9e18ed3 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -696,6 +696,7 @@ final class WP_Customize_Nav_Menus { $this->manager->add_setting( new WP_Customize_Filter_Setting( $this->manager, 'nav_menus_created_posts', array( 'transport' => 'postMessage', + 'type' => 'option', // To prevent theme prefix in changeset. 'default' => array(), 'sanitize_callback' => array( $this, 'sanitize_nav_menus_created_posts' ), ) ) ); diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 9ca52735cb..d892ea73d8 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1824,26 +1824,26 @@ function get_theme_starter_content() { $config = array(); } - $core_content = array ( + $core_content = array( 'widgets' => array( - 'text_business_info' => array ( 'text', array ( + 'text_business_info' => array( 'text', array( 'title' => _x( 'Find Us', 'Theme starter content' ), - 'text' => join( '', array ( + 'text' => join( '', array( '

' . _x( 'Address', 'Theme starter content' ) . '
', _x( '123 Main Street', 'Theme starter content' ) . '
' . _x( 'New York, NY 10001', 'Theme starter content' ) . '

', '

' . _x( 'Hours', 'Theme starter content' ) . '
', _x( 'Monday—Friday: 9:00AM–5:00PM', 'Theme starter content' ) . '
' . _x( 'Saturday & Sunday: 11:00AM–3:00PM', 'Theme starter content' ) . '

' ) ), ) ), - 'search' => array ( 'search', array ( + 'search' => array( 'search', array( 'title' => _x( 'Site Search', 'Theme starter content' ), ) ), - 'text_credits' => array ( 'text', array ( + 'text_credits' => array( 'text', array( 'title' => _x( 'Site Credits', 'Theme starter content' ), 'text' => sprintf( _x( 'This site was created on %s', 'Theme starter content' ), get_date_from_gmt( current_time( 'mysql', 1 ), 'c' ) ), ) ), ), - 'nav_menus' => array ( + 'nav_menus' => array( 'page_home' => array( 'type' => 'post_type', 'object' => 'page', @@ -1919,48 +1919,53 @@ function get_theme_starter_content() { foreach ( $config as $type => $args ) { switch( $type ) { - // Use options and theme_mods as-is + // Use options and theme_mods as-is. case 'options' : case 'theme_mods' : $content[ $type ] = $config[ $type ]; break; - // Widgets are an extra level down due to groupings + // Widgets are grouped into sidebars. case 'widgets' : - foreach ( $config[ $type ] as $group => $items ) { - foreach ( $items as $id ) { - if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) { - $content[ $type ][ $group ][ $id ] = $core_content[ $type ][ $id ]; + foreach ( $config[ $type ] as $sidebar_id => $widgets ) { + foreach ( $widgets as $widget ) { + if ( is_array( $widget ) ) { + $content[ $type ][ $sidebar_id ][] = $widget; + } elseif ( is_string( $widget ) && ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $widget ] ) ) { + $content[ $type ][ $sidebar_id ][] = $core_content[ $type ][ $widget ]; } } } break; - // And nav menus are yet another level down + // And nav menu items are grouped into nav menus. case 'nav_menus' : - foreach ( $config[ $type ] as $group => $args2 ) { - // Menu groups need a name - if ( empty( $args['name'] ) ) { - $args2['name'] = $group; + foreach ( $config[ $type ] as $nav_menu_location => $nav_menu ) { + + // Ensure nav menus get a name. + if ( empty( $nav_menu['name'] ) ) { + $nav_menu['name'] = $nav_menu_location; } - $content[ $type ][ $group ]['name'] = $args2['name']; + $content[ $type ][ $nav_menu_location ]['name'] = $nav_menu['name']; - // Do we need to check if this is empty? - foreach ( $args2['items'] as $id ) { - if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) { - $content[ $type ][ $group ]['items'][ $id ] = $core_content[ $type ][ $id ]; + foreach ( $nav_menu['items'] as $nav_menu_item ) { + if ( is_array( $nav_menu_item ) ) { + $content[ $type ][ $nav_menu_location ]['items'][] = $nav_menu_item; + } elseif ( is_string( $nav_menu_item ) && ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $nav_menu_item ] ) ) { + $content[ $type ][ $nav_menu_location ]['items'][] = $core_content[ $type ][ $nav_menu_item ]; } } } break; - - // Everything else should map at the next level + // Everything else should map at the next level. default : - foreach( $config[ $type ] as $id ) { - if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) { - $content[ $type ][ $id ] = $core_content[ $type ][ $id ]; + foreach( $config[ $type ] as $i => $item ) { + if ( is_array( $item ) ) { + $content[ $type ][ $i ] = $item; + } elseif ( is_string( $item ) && ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $item ] ) ) { + $content[ $type ][ $item ] = $core_content[ $type ][ $item ]; } } break;