: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$changeset_date_gmt = get_gmt_from_date( $changeset_date );
$timestamp = strtotime( $changeset_date );
wp_send_json_error( 'bad_customize_changeset_date', 400 );
$changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
$autosave = ! empty( $_POST['customize_changeset_autosave'] );
if ( ! $is_new_changeset ) {
$lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
// Force request to autosave when changeset is locked.
if ( $lock_user_id && ! $autosave ) {
$changeset_status = null;
$changeset_date_gmt = null;
if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
define( 'DOING_AUTOSAVE', true );
$r = $this->save_changeset_post(
'status' => $changeset_status,
'title' => $changeset_title,
'date_gmt' => $changeset_date_gmt,
'data' => $input_changeset_data,
if ( $autosave && ! is_wp_error( $r ) ) {
// If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
if ( $lock_user_id && ! is_wp_error( $r ) ) {
__( 'Changeset is being edited by other user.' ),
'lock_user' => $this->get_lock_user_data( $lock_user_id ),
if ( is_wp_error( $r ) ) {
'message' => $r->get_error_message(),
'code' => $r->get_error_code(),
if ( is_array( $r->get_error_data() ) ) {
$response = array_merge( $response, $r->get_error_data() );
$response['data'] = $r->get_error_data();
$changeset_post = get_post( $this->changeset_post_id() );
// Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
if ( $is_new_changeset ) {
$this->dismiss_user_auto_draft_changesets();
// Note that if the changeset status was publish, then it will get set to Trash if revisions are not supported.
$response['changeset_status'] = $changeset_post->post_status;
if ( $is_publish && 'trash' === $response['changeset_status'] ) {
$response['changeset_status'] = 'publish';
if ( 'publish' !== $response['changeset_status'] ) {
$this->set_changeset_lock( $changeset_post->ID );
if ( 'future' === $response['changeset_status'] ) {
$response['changeset_date'] = $changeset_post->post_date;
if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
$response['next_changeset_uuid'] = wp_generate_uuid4();
$response['autosaved'] = $autosaved;
if ( isset( $response['setting_validities'] ) ) {
$response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
* Filters response data for a successful customize_save Ajax request.
* This filter does not apply if there was a nonce or authentication failure.
* @param array $response Additional information passed back to the 'saved'
* event on `wp.customize`.
* @param WP_Customize_Manager $manager WP_Customize_Manager instance.
$response = apply_filters( 'customize_save_response', $response, $this );
if ( is_wp_error( $r ) ) {
wp_send_json_error( $response );
wp_send_json_success( $response );
* Saves the post for the loaded changeset.
* 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 bool $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
* @type bool $autosave Whether this is a request to create an autosave revision.
* @return array|WP_Error Returns array on success and WP_Error with array data on error.
public function save_changeset_post( $args = array() ) {
'user_id' => get_current_user_id(),
'starter_content' => false,
$changeset_post_id = $this->changeset_post_id();
$existing_changeset_data = array();
if ( $changeset_post_id ) {
$existing_status = get_post_status( $changeset_post_id );
if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
'changeset_already_published',
__( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ),
'next_changeset_uuid' => wp_generate_uuid4(),
$existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
if ( is_wp_error( $existing_changeset_data ) ) {
return $existing_changeset_data;
// Fail if attempting to publish but publish hook is missing.
if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
return new WP_Error( 'missing_publish_callback' );
$now = gmdate( 'Y-m-d H:i:59' );
if ( $args['date_gmt'] ) {
$is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
if ( ! $is_future_dated ) {
return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
$will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
if ( $will_remain_auto_draft ) {
return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
} elseif ( $changeset_post_id && 'future' === $args['status'] ) {
// Fail if the new status is future but the existing post's date is not in the future.
$changeset_post = get_post( $changeset_post_id );
if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
if ( ! empty( $is_future_dated ) && 'publish' === $args['status'] ) {
$args['status'] = 'future';
// Validate autosave param. See _wp_post_revision_fields() for why these fields are disallowed.
if ( $args['autosave'] ) {
if ( $args['date_gmt'] ) {
return new WP_Error( 'illegal_autosave_with_date_gmt' );
} elseif ( $args['status'] ) {
return new WP_Error( 'illegal_autosave_with_status' );
} elseif ( $args['user_id'] && get_current_user_id() !== $args['user_id'] ) {
return new WP_Error( 'illegal_autosave_with_non_current_user' );
// The request was made via wp.customize.previewer.save().
$update_transactionally = (bool) $args['status'];
$allow_revision = (bool) $args['status'];
// Amend post values with any supplied data.
foreach ( $args['data'] as $setting_id => $setting_params ) {
if ( is_array( $setting_params ) && array_key_exists( 'value', $setting_params ) ) {
$this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
// Note that in addition to post data, this will include any stashed theme mods.
$post_values = $this->unsanitized_post_values(
'exclude_changeset' => true,
'exclude_post_data' => false,
$this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
* Get list of IDs for settings that have values different from what is currently
* saved in the changeset. By skipping any values that are already the same, the
* subset of changed settings can be passed into validate_setting_values to prevent
* an underprivileged modifying a single setting for which they have the capability
* from being blocked from saving. This also prevents a user from touching of the
* previous saved settings and overriding the associated user_id if they made no change.
$changed_setting_ids = array();
foreach ( $post_values as $setting_id => $setting_value ) {
$setting = $this->get_setting( $setting_id );
if ( $setting && 'theme_mod' === $setting->type ) {
$prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
$prefixed_setting_id = $setting_id;
! isset( $existing_changeset_data[ $prefixed_setting_id ] )
! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
$existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
if ( $is_value_changed ) {
$changed_setting_ids[] = $setting_id;
* Fires before save validation happens.
* Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
* at this point to catch any settings registered after `customize_register`.
* The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
* @param WP_Customize_Manager $manager WP_Customize_Manager instance.
do_action( 'customize_save_validation_before', $this );
$validated_values = array_merge(
array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
$setting_validities = $this->validate_setting_values(
'validate_capability' => true,
'validate_existence' => true,
$invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
* Short-circuit if there are invalid settings the update is transactional.
* A changeset update is transactional when a status is supplied in the request.
if ( $update_transactionally && $invalid_setting_count > 0 ) {
'setting_validities' => $setting_validities,
/* translators: %s: Number of invalid settings. */
'message' => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
return new WP_Error( 'transaction_fail', '', $response );
// Obtain/merge data for changeset.
$original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
$data = $original_changeset_data;
if ( is_wp_error( $data ) ) {
// Ensure that all post values are included in the changeset data.
foreach ( $post_values as $setting_id => $post_value ) {
if ( ! isset( $args['data'][ $setting_id ] ) ) {
$args['data'][ $setting_id ] = array();
if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
$args['data'][ $setting_id ]['value'] = $post_value;
foreach ( $args['data'] as $setting_id => $setting_params ) {
$setting = $this->get_setting( $setting_id );
if ( ! $setting || ! $setting->check_capabilities() ) {
// Skip updating changeset for invalid setting values.
if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
$changeset_setting_id = $setting_id;
if ( 'theme_mod' === $setting->type ) {
$changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
if ( null === $setting_params ) {
// Remove setting from changeset entirely.
unset( $data[ $changeset_setting_id ] );
if ( ! isset( $data[ $changeset_setting_id ] ) ) {
$data[ $changeset_setting_id ] = array();
// Merge any additional setting params that have been supplied with the existing params.
$merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
// Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
$data[ $changeset_setting_id ] = array_merge(
'type' => $setting->type,
'user_id' => $args['user_id'],
'date_modified_gmt' => current_time( 'mysql', true ),
// 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'] );
'uuid' => $this->changeset_uuid(),
'title' => $args['title'],
'status' => $args['status'],
'date_gmt' => $args['date_gmt'],
'post_id' => $changeset_post_id,
'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
* Filters the settings' data that will be persisted into the changeset.
* Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
* @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
* @param array $context {
* @type string $uuid Changeset UUID.
* @type string $title Requested title for the changeset post.
* @type string $status Requested status for the changeset post.
* @type string $date_gmt Requested date for the changeset post in MySQL format and GMT timezone.
* @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet.
* @type array $previous_data Previous data contained in the changeset.
* @type WP_Customize_Manager $manager Manager instance.
$data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
// Switch theme if publishing changes now.
if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
// Temporarily stop previewing the theme to allow switch_themes() to operate properly.
$this->stop_previewing_theme();
switch_theme( $this->get_stylesheet() );
update_option( 'theme_switched_via_customizer', true );
$this->start_previewing_theme();
// Gather the data for wp_insert_post()/wp_update_post().
// JSON_UNESCAPED_SLASHES is only to improve readability as slashes needn't be escaped in storage.
'post_content' => wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ),
$post_array['post_title'] = $args['title'];
if ( $changeset_post_id ) {
$post_array['ID'] = $changeset_post_id;
$post_array['post_type'] = 'customize_changeset';
$post_array['post_name'] = $this->changeset_uuid();
$post_array['post_status'] = 'auto-draft';
$post_array['post_status'] = $args['status'];
// Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
if ( 'publish' === $args['status'] ) {
$post_array['post_date_gmt'] = '0000-00-00 00:00:00';
$post_array['post_date'] = '0000-00-00 00:00:00';
} elseif ( $args['date_gmt'] ) {
$post_array['post_date_gmt'] = $args['date_gmt'];
$post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
} elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
* Keep bumping the date for the auto-draft whenever it is modified;
* this extends its life, preserving it from garbage-collection via
* wp_delete_auto_drafts().
$post_array['post_date'] = current_time( 'mysql' );
$post_array['post_date_gmt'] = '';
$this->store_changeset_revision = $allow_revision;
add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
* Update the changeset post. The publish_customize_changeset action will cause the settings in the
* changeset to be saved via WP_Customize_Setting::save(). Updating a post with publish status will
* trigger WP_Customize_Manager::publish_changeset_values().
add_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5, 3 );
if ( $changeset_post_id ) {
if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) {
// See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability.
add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 );
$post_array['post_ID'] = $post_array['ID'];
$post_array['post_type'] = 'customize_changeset';
$r = wp_create_post_autosave( wp_slash( $post_array ) );
remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 );
$post_array['edit_date'] = true; // Prevent date clearing.
$r = wp_update_post( wp_slash( $post_array ), true );
// Delete autosave revision for user when the changeset is updated.
if ( ! empty( $args['user_id'] ) ) {
$autosave_draft = wp_get_post_autosave( $changeset_post_id, $args['user_id'] );
wp_delete_post( $autosave_draft->ID, true );
$r = wp_insert_post( wp_slash( $post_array ), true );
if ( ! is_wp_error( $r ) ) {
$this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
remove_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5 );
$this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
'setting_validities' => $setting_validities,
if ( is_wp_error( $r ) ) {