Saves the post for the loaded changeset.
Parameters
$args
arrayoptional- Args for changeset post.
data
arrayOptional additional changeset data. Values will be merged on top of any existing post values.status
stringPost status. Optional. If supplied, the save will be transactional and a post revision will be allowed.title
stringPost title. Optional.date_gmt
stringDate in GMT. Optional.user_id
intID for user who is saving the changeset. Optional, defaults to the current user ID.starter_content
boolWhether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.autosave
boolWhether this is a request to create an autosave revision.
Default:
array()
Source
public function save_changeset_post( $args = array() ) {
$args = array_merge(
array(
'status' => null,
'title' => null,
'data' => array(),
'date_gmt' => null,
'user_id' => get_current_user_id(),
'starter_content' => false,
'autosave' => false,
),
$args
);
$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 ) {
return new WP_Error(
'changeset_already_published',
__( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ),
array(
'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' );
}
// Validate date.
$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(
array(
'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;
} else {
$prefixed_setting_id = $setting_id;
}
$is_value_changed = (
! 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 '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.
*
* @since 4.6.0
*
* @param WP_Customize_Manager $manager WP_Customize_Manager instance.
*/
do_action( 'customize_save_validation_before', $this );
// Validate settings.
$validated_values = array_merge(
array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
$post_values
);
$setting_validities = $this->validate_setting_values(
$validated_values,
array(
'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 ) {
$response = array(
'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 ) ) {
$data = array();
}
// 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() ) {
continue;
}
// Skip updating changeset for invalid setting values.
if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
continue;
}
$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 ] );
} else {
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 ) {
continue;
}
$data[ $changeset_setting_id ] = array_merge(
$merged_setting_params,
array(
'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'] );
}
}
}
$filter_context = array(
'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,
'manager' => $this,
);
/**
* 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.
*
* @since 4.7.0
*
* @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
* @param array $context {
* Filter 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().
$post_array = array(
// 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 ),
);
if ( $args['title'] ) {
$post_array['post_title'] = $args['title'];
}
if ( $changeset_post_id ) {
$post_array['ID'] = $changeset_post_id;
} else {
$post_array['post_type'] = 'customize_changeset';
$post_array['post_name'] = $this->changeset_uuid();
$post_array['post_status'] = 'auto-draft';
}
if ( $args['status'] ) {
$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 );
} else {
$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'] );
if ( $autosave_draft ) {
wp_delete_post( $autosave_draft->ID, true );
}
}
}
} else {
$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' ) );
$response = array(
'setting_validities' => $setting_validities,
);
if ( is_wp_error( $r ) ) {
$response['changeset_post_save_failure'] = $r->get_error_code();
return new WP_Error( 'changeset_post_save_failure', '', $response );
}
return $response;
}
Hooks
- apply_filters( ‘customize_changeset_save_data’,
array $data ,array $context ) Filters the settings’ data that will be persisted into the changeset.
- do_action( ‘customize_save_validation_before’,
WP_Customize_Manager $manager ) Fires before save validation happens.
Changelog
Version | Description |
---|---|
4.7.0 | Introduced. |
User Contributed Notes
You must log in before being able to contribute a note or feedback.