: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'lazy_load_term_meta' => false,
if ( ! empty( $changeset_post_query->posts ) ) {
// Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
$changeset_post_id = $changeset_post_query->posts[0]->ID;
wp_cache_set( $uuid, $changeset_post_id, $cache_group );
return $changeset_post_id;
* Args to pass into `get_posts()` to query changesets.
* @type int $posts_per_page Number of posts to return. Defaults to -1 (all posts).
* @type int $author Post author. Defaults to current user.
* @type string $post_status Status of changeset. Defaults to 'auto-draft'.
* @type bool $exclude_restore_dismissed Whether to exclude changeset auto-drafts that have been dismissed. Defaults to true.
* @return WP_Post[] Auto-draft changesets.
protected function get_changeset_posts( $args = array() ) {
'exclude_restore_dismissed' => true,
'post_type' => 'customize_changeset',
'post_status' => 'auto-draft',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'lazy_load_term_meta' => false,
if ( get_current_user_id() ) {
$default_args['author'] = get_current_user_id();
$args = array_merge( $default_args, $args );
if ( ! empty( $args['exclude_restore_dismissed'] ) ) {
unset( $args['exclude_restore_dismissed'] );
$args['meta_query'] = array(
'key' => '_customize_restore_dismissed',
'compare' => 'NOT EXISTS',
return get_posts( $args );
* Dismisses all of the current user's auto-drafts (other than the present one).
* @return int The number of auto-drafts that were dismissed.
protected function dismiss_user_auto_draft_changesets() {
$changeset_autodraft_posts = $this->get_changeset_posts(
'post_status' => 'auto-draft',
'exclude_restore_dismissed' => true,
foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) {
if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
* Gets the changeset post ID for the loaded changeset.
* @return int|null Post ID on success or null if there is no post yet saved.
public function changeset_post_id() {
if ( ! isset( $this->_changeset_post_id ) ) {
$post_id = $this->find_changeset_post_id( $this->changeset_uuid() );
$this->_changeset_post_id = $post_id;
if ( false === $this->_changeset_post_id ) {
return $this->_changeset_post_id;
* Gets the data stored in a changeset post.
* @param int $post_id Changeset post ID.
* @return array|WP_Error Changeset data or WP_Error on error.
protected function get_changeset_post_data( $post_id ) {
return new WP_Error( 'empty_post_id' );
$changeset_post = get_post( $post_id );
if ( ! $changeset_post ) {
return new WP_Error( 'missing_post' );
if ( 'revision' === $changeset_post->post_type ) {
if ( 'customize_changeset' !== get_post_type( $changeset_post->post_parent ) ) {
return new WP_Error( 'wrong_post_type' );
} elseif ( 'customize_changeset' !== $changeset_post->post_type ) {
return new WP_Error( 'wrong_post_type' );
$changeset_data = json_decode( $changeset_post->post_content, true );
$last_error = json_last_error();
return new WP_Error( 'json_parse_error', '', $last_error );
if ( ! is_array( $changeset_data ) ) {
return new WP_Error( 'expected_array' );
* @since 4.9.0 This will return the changeset's data with a user's autosave revision merged on top, if one exists and $autosaved is true.
* @return array Changeset data.
public function changeset_data() {
if ( isset( $this->_changeset_data ) ) {
return $this->_changeset_data;
$changeset_post_id = $this->changeset_post_id();
if ( ! $changeset_post_id ) {
$this->_changeset_data = array();
if ( $this->autosaved() && is_user_logged_in() ) {
$autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
$data = $this->get_changeset_post_data( $autosave_post->ID );
if ( ! is_wp_error( $data ) ) {
$this->_changeset_data = $data;
// Load data from the changeset if it was not loaded from an autosave.
if ( ! isset( $this->_changeset_data ) ) {
$data = $this->get_changeset_post_data( $changeset_post_id );
if ( ! is_wp_error( $data ) ) {
$this->_changeset_data = $data;
$this->_changeset_data = array();
return $this->_changeset_data;
* Starter content setting IDs.
protected $pending_starter_content_settings_ids = array();
* Imports theme starter content into the customized state.
* @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
public function import_theme_starter_content( $starter_content = array() ) {
if ( empty( $starter_content ) ) {
$starter_content = get_theme_starter_content();
$changeset_data = array();
if ( $this->changeset_post_id() ) {
* Don't re-import starter content into a changeset saved persistently.
* This will need to be revisited in the future once theme switching
* is allowed with drafted/scheduled changesets, since switching to
* another theme could result in more starter content being applied.
* However, when doing an explicit save it is currently possible for
* nav menus and nav menu items specifically to lose their starter_content
* flags, thus resulting in duplicates being created since they fail
* to get re-used. See #40146.
if ( 'auto-draft' !== get_post_status( $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();
$attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
$posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
$options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
$nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
$theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
$max_widget_numbers = array();
foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
$sidebar_widget_ids = array();
foreach ( $widgets as $widget ) {
list( $id_base, $instance ) = $widget;
if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
// When $settings is an array-like object, get an intrinsic array for use with array_keys().
$settings = get_option( "widget_{$id_base}", array() );
if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
$settings = $settings->getArrayCopy();
unset( $settings['_multiwidget'] );
// Find the max widget number for this type.
$widget_numbers = array_keys( $settings );
if ( count( $widget_numbers ) > 0 ) {
$max_widget_numbers[ $id_base ] = max( ...$widget_numbers );
$max_widget_numbers[ $id_base ] = 1;
$max_widget_numbers[ $id_base ] += 1;
$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 ] );
$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 );
$this->pending_starter_content_settings_ids[] = $setting_id;
$sidebar_widget_ids[] = $widget_id;
$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->pending_starter_content_settings_ids[] = $setting_id;
$starter_content_auto_draft_post_ids = array();
if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
// Make an index of all the posts needed and what their slugs are.
$attachments = $this->prepare_starter_content_attachments( $attachments );
foreach ( $attachments as $attachment ) {
$key = 'attachment:' . $attachment['post_name'];
$needed_posts[ $key ] = true;
foreach ( array_keys( $posts ) as $post_symbol ) {
if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
unset( $posts[ $post_symbol ] );
if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
$posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
$posts[ $post_symbol ]['post_type'] = 'post';
$needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
$all_post_slugs = array_merge(
wp_list_pluck( $attachments, 'post_name' ),
wp_list_pluck( $posts, 'post_name' )
* Obtain all post types referenced in starter content to use in query.
* This is needed because 'any' will not account for post types not yet registered.
$post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
// Re-use auto-draft starter content posts referenced in the current customized state.
$existing_starter_content_posts = array();
if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
$existing_posts_query = new WP_Query(
'post__in' => $starter_content_auto_draft_post_ids,
'post_status' => 'auto-draft',
'post_type' => $post_types,
foreach ( $existing_posts_query->posts as $existing_post ) {
$post_name = $existing_post->post_name;
if ( empty( $post_name ) ) {
$post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
$existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
// Re-use non-auto-draft posts.
if ( ! empty( $all_post_slugs ) ) {
$existing_posts_query = new WP_Query(
'post_name__in' => $all_post_slugs,
'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
foreach ( $existing_posts_query->posts as $existing_post ) {
$key = $existing_post->post_type . ':' . $existing_post->post_name;
if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
$existing_starter_content_posts[ $key ] = $existing_post;
// Attachments are technically posts but handled differently.
if ( ! empty( $attachments ) ) {
$attachment_ids = array();
foreach ( $attachments as $symbol => $attachment ) {
'name' => $attachment['file_name'],
$file_path = $attachment['file_path'];
if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
$attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
$attachment_id = $attachment_post->ID;
$attached_file = get_attached_file( $attachment_id );
if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
} elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
// Re-generate attachment metadata since it was previously generated for a different theme.
$metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
wp_update_attachment_metadata( $attachment_id, $metadata );
update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
// Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
if ( ! $attachment_id ) {
// Copy file to temp location so that original file won't get deleted from theme after sideloading.
$temp_file_name = wp_tempnam( wp_basename( $file_path ) );
if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
$file_array['tmp_name'] = $temp_file_name;
if ( empty( $file_array['tmp_name'] ) ) {
$attachment_post_data = array_merge(
wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
$attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
if ( is_wp_error( $attachment_id ) ) {
update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
$attachment_ids[ $symbol ] = $attachment_id;
$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
if ( ! empty( $posts ) ) {
foreach ( array_keys( $posts ) as $post_symbol ) {
if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
$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'] );
// Use existing auto-draft post if one already exists with the same type and name.
if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
$posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
// Translate the featured image symbol.
if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
&& preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
&& isset( $attachment_ids[ $matches['symbol'] ] ) ) {
$posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
$posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
$r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
if ( $r instanceof WP_Post ) {
$posts[ $post_symbol ]['ID'] = $r->ID;
$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
// The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
$setting_id = 'nav_menus_created_posts';
$this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
$this->pending_starter_content_settings_ids[] = $setting_id;
$reused_nav_menu_setting_ids = array();
foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
$nav_menu_term_id = null;
$nav_menu_setting_id = null;
// Look for an existing placeholder menu with starter content to re-use.
foreach ( $changeset_data as $setting_id => $setting_params ) {
! empty( $setting_params['starter_content'] )
! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
$nav_menu_term_id = (int) $matches['nav_menu_id'];
$nav_menu_setting_id = $setting_id;
$reused_nav_menu_setting_ids[] = $setting_id;
if ( ! $nav_menu_term_id ) {
while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
$nav_menu_term_id = $placeholder_id;
$nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,