: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
// @todo Add support for menu_item_parent.
foreach ( $nav_menu['items'] as $nav_menu_item ) {
$nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
if ( ! isset( $nav_menu_item['position'] ) ) {
$nav_menu_item['position'] = $position++;
$nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
if ( isset( $nav_menu_item['object_id'] ) ) {
if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
$nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
if ( empty( $nav_menu_item['title'] ) ) {
$original_object = get_post( $nav_menu_item['object_id'] );
$nav_menu_item['title'] = $original_object->post_title;
$nav_menu_item['object_id'] = 0;
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->pending_starter_content_settings_ids[] = $nav_menu_item_setting_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->pending_starter_content_settings_ids[] = $setting_id;
foreach ( $options as $name => $value ) {
// Serialize the value to check for post symbols.
$value = maybe_serialize( $value );
if ( is_serialized( $value ) ) {
if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
if ( isset( $posts[ $matches['symbol'] ] ) ) {
$symbol_match = $posts[ $matches['symbol'] ]['ID'];
} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
$symbol_match = $attachment_ids[ $matches['symbol'] ];
// If we have any symbol matches, update the values.
if ( isset( $symbol_match ) ) {
// Replace found string matches with post IDs.
$value = str_replace( $matches[0], "i:{$symbol_match}", $value );
} elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
if ( isset( $posts[ $matches['symbol'] ] ) ) {
$value = $posts[ $matches['symbol'] ]['ID'];
} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
$value = $attachment_ids[ $matches['symbol'] ];
// Unserialize values after checking for post symbols, so they can be properly referenced.
$value = maybe_unserialize( $value );
if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
$this->set_post_value( $name, $value );
$this->pending_starter_content_settings_ids[] = $name;
foreach ( $theme_mods as $name => $value ) {
// Serialize the value to check for post symbols.
$value = maybe_serialize( $value );
// Check if value was serialized.
if ( is_serialized( $value ) ) {
if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
if ( isset( $posts[ $matches['symbol'] ] ) ) {
$symbol_match = $posts[ $matches['symbol'] ]['ID'];
} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
$symbol_match = $attachment_ids[ $matches['symbol'] ];
// If we have any symbol matches, update the values.
if ( isset( $symbol_match ) ) {
// Replace found string matches with post IDs.
$value = str_replace( $matches[0], "i:{$symbol_match}", $value );
} elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
if ( isset( $posts[ $matches['symbol'] ] ) ) {
$value = $posts[ $matches['symbol'] ]['ID'];
} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
$value = $attachment_ids[ $matches['symbol'] ];
// Unserialize values after checking for post symbols, so they can be properly referenced.
$value = maybe_unserialize( $value );
// Handle header image as special case since setting has a legacy format.
if ( 'header_image' === $name ) {
$name = 'header_image_data';
$metadata = wp_get_attachment_metadata( $value );
if ( empty( $metadata ) ) {
'attachment_id' => $value,
'url' => wp_get_attachment_url( $value ),
'height' => $metadata['height'],
'width' => $metadata['width'],
} elseif ( 'background_image' === $name ) {
$value = wp_get_attachment_url( $value );
if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
$this->set_post_value( $name, $value );
$this->pending_starter_content_settings_ids[] = $name;
if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
if ( did_action( 'customize_register' ) ) {
$this->_save_starter_content_changeset();
add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
* Prepares starter content attachments.
* Ensure that the attachments are valid and that they have slugs and file name/path.
* @param array $attachments Attachments.
* @return array Prepared attachments.
protected function prepare_starter_content_attachments( $attachments ) {
$prepared_attachments = array();
if ( empty( $attachments ) ) {
return $prepared_attachments;
// Such is The WordPress Way.
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
foreach ( $attachments as $symbol => $attachment ) {
// A file is required and URLs to files are not currently allowed.
if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
if ( file_exists( $attachment['file'] ) ) {
$file_path = $attachment['file']; // Could be absolute path to file in plugin.
} elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
$file_path = get_stylesheet_directory() . '/' . $attachment['file'];
} elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
$file_path = get_template_directory() . '/' . $attachment['file'];
$file_name = wp_basename( $attachment['file'] );
// Skip file types that are not recognized.
$checked_filetype = wp_check_filetype( $file_name );
if ( empty( $checked_filetype['type'] ) ) {
// Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
if ( empty( $attachment['post_name'] ) ) {
if ( ! empty( $attachment['post_title'] ) ) {
$attachment['post_name'] = sanitize_title( $attachment['post_title'] );
$attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
$attachment['file_name'] = $file_name;
$attachment['file_path'] = $file_path;
$prepared_attachments[ $symbol ] = $attachment;
return $prepared_attachments;
* Saves starter content changeset.
public function _save_starter_content_changeset() {
if ( empty( $this->pending_starter_content_settings_ids ) ) {
$this->save_changeset_post(
'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
'starter_content' => true,
$this->saved_starter_content_changeset = true;
$this->pending_starter_content_settings_ids = array();
* Gets dirty pre-sanitized setting values in the current customized state.
* The returned array consists of a merge of three sources:
* 1. If the theme is not currently active, then the base array is any stashed
* theme mods that were modified previously but never published.
* 2. The values from the current changeset, if it exists.
* 3. If the user can customize, the values parsed from the incoming
* `$_POST['customized']` JSON data.
* 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
* The name "unsanitized_post_values" is a carry-over from when the customized
* state was exclusively sourced from `$_POST['customized']`. Nevertheless,
* the value returned will come from the current changeset post and from the
* @since 4.7.0 Added `$args` parameter and merging with changeset values and stashed theme mods.
* @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
* @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
public function unsanitized_post_values( $args = array() ) {
'exclude_changeset' => false,
'exclude_post_data' => ! current_user_can( 'customize' ),
// Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
if ( ! $this->is_theme_active() ) {
$stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
$stylesheet = $this->get_stylesheet();
if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
$values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
if ( ! $args['exclude_changeset'] ) {
foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
if ( ! array_key_exists( 'value', $setting_params ) ) {
if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
// Ensure that theme mods values are only used if they were saved under the active theme.
$namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
$values[ $matches['setting_id'] ] = $setting_params['value'];
$values[ $setting_id ] = $setting_params['value'];
if ( ! $args['exclude_post_data'] ) {
if ( ! isset( $this->_post_values ) ) {
if ( isset( $_POST['customized'] ) ) {
$post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
if ( is_array( $post_values ) ) {
$this->_post_values = $post_values;
$this->_post_values = array();
$values = array_merge( $values, $this->_post_values );
* Returns the sanitized value for a given setting from the current customized state.
* The name "post_value" is a carry-over from when the customized state was exclusively
* sourced from `$_POST['customized']`. Nevertheless, the value returned will come
* from the current changeset post and from the incoming post data.
* @since 4.1.1 Introduced the `$default_value` parameter.
* @since 4.6.0 `$default_value` is now returned early when the setting post value is invalid.
* @see WP_REST_Server::dispatch()
* @see WP_REST_Request::sanitize_params()
* @see WP_REST_Request::has_valid_params()
* @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
* @param mixed $default_value Value returned if `$setting` has no post value (added in 4.2.0)
* or the post value is invalid (added in 4.6.0).
* @return string|mixed Sanitized value or the `$default_value` provided.
public function post_value( $setting, $default_value = null ) {
$post_values = $this->unsanitized_post_values();
if ( ! array_key_exists( $setting->id, $post_values ) ) {
$value = $post_values[ $setting->id ];
$valid = $setting->validate( $value );
if ( is_wp_error( $valid ) ) {
$value = $setting->sanitize( $value );
if ( is_null( $value ) || is_wp_error( $value ) ) {
* Overrides a setting's value in the current customized state.
* The name "post_value" is a carry-over from when the customized state was
* exclusively sourced from `$_POST['customized']`.
* @param string $setting_id ID for the WP_Customize_Setting instance.
* @param mixed $value Post value.
public function set_post_value( $setting_id, $value ) {
$this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
$this->_post_values[ $setting_id ] = $value;
* Announces when a specific setting's unsanitized post value has been set.
* Fires when the WP_Customize_Manager::set_post_value() method is called.
* The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
* @param mixed $value Unsanitized setting post value.
* @param WP_Customize_Manager $manager WP_Customize_Manager instance.
do_action( "customize_post_value_set_{$setting_id}", $value, $this );
* Announces when any setting's unsanitized post value has been set.
* Fires when the WP_Customize_Manager::set_post_value() method is called.
* This is useful for `WP_Customize_Setting` instances to watch
* in order to update a cached previewed value.
* @param string $setting_id Setting ID.
* @param mixed $value Unsanitized setting post value.
* @param WP_Customize_Manager $manager WP_Customize_Manager instance.
do_action( 'customize_post_value_set', $setting_id, $value, $this );
* Prints JavaScript settings.
public function customize_preview_init() {
* Now that Customizer previews are loaded into iframes via GET requests
* and natural URLs with transaction UUIDs added, we need to ensure that
* the responses are never cached by proxies. In practice, this will not
* be needed if the user is logged-in anyway. But if anonymous access is
* allowed then the auth cookies would not be sent and WordPress would
* not send no-cache headers by default.
if ( ! headers_sent() ) {
header( 'X-Robots: noindex, nofollow, noarchive' );
header( 'X-Robots-Tag: noindex, nofollow, noarchive' );
add_filter( 'wp_robots', 'wp_robots_no_robots' );
add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
* If preview is being served inside the customizer preview iframe, and
* if the user doesn't have customize capability, then it is assumed
* that the user's session has expired and they need to re-authenticate.
if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
/* translators: %s: customize_messenger_channel */
__( 'Unauthorized. You may remove the %s param to preview as frontend.' ),
'<code>customize_messenger_channel<code>'
$this->prepare_controls();
add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
wp_enqueue_script( 'customize-preview' );
wp_enqueue_style( 'customize-preview' );
add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
add_filter( 'get_edit_post_link', '__return_empty_string' );
* Fires once the Customizer preview has initialized and JavaScript
* settings have been printed.
* @param WP_Customize_Manager $manager WP_Customize_Manager instance.
do_action( 'customize_preview_init', $this );
* Filters the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
* @param array $headers Headers.
public function filter_iframe_security_headers( $headers ) {
$headers['X-Frame-Options'] = 'SAMEORIGIN';
$headers['Content-Security-Policy'] = "frame-ancestors 'self'";
* Adds customize state query params to a given URL if preview is allowed.
* @see WP_Customize_Manager::get_allowed_url()
* @param string $url URL.
public function add_state_query_params( $url ) {
$parsed_original_url = wp_parse_url( $url );
foreach ( $this->get_allowed_urls() as $allowed_url ) {
$parsed_allowed_url = wp_parse_url( $allowed_url );
$parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
$parsed_allowed_url['host'] === $parsed_original_url['host']
str_starts_with( $parsed_original_url['path'], $parsed_allowed_url['path'] )