WP_Customize_Manager::save_changeset_post PHP Method

save_changeset_post() public method

Save the post for the loaded changeset.
Since: 4.7.0
public save_changeset_post ( array $args = [] ) : array | WP_Error
$args array { 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. }
return array | WP_Error Returns array on success and WP_Error with array data on error.
    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), $args);
        $changeset_post_id = $this->changeset_post_id();
        $existing_changeset_data = array();
        if ($changeset_post_id) {
            $existing_changeset_data = $this->get_changeset_post_data($changeset_post_id);
        }
        // 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 (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 {@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.
         *
         * @since 4.6.0
         *
         * @param WP_Customize_Manager $this 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), $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, 'message' => sprintf(_n('There is %s invalid setting.', 'There are %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']));
                // 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().
        $json_options = 0;
        if (defined('JSON_UNESCAPED_SLASHES')) {
            $json_options |= JSON_UNESCAPED_SLASHES;
            // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
        }
        $json_options |= JSON_PRETTY_PRINT;
        // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
        $post_array = array('post_content' => wp_json_encode($data, $json_options));
        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'];
        }
        if ($args['date_gmt']) {
            $post_array['post_date_gmt'] = $args['date_gmt'];
            $post_array['post_date'] = get_date_from_gmt($args['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().
        $has_kses = false !== has_filter('content_save_pre', 'wp_filter_post_kses');
        if ($has_kses) {
            kses_remove_filters();
            // Prevent KSES from corrupting JSON in post_content.
        }
        // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
        if ($changeset_post_id) {
            $post_array['edit_date'] = true;
            // Prevent date clearing.
            $r = wp_update_post(wp_slash($post_array), 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.
            }
        }
        if ($has_kses) {
            kses_init_filters();
        }
        $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;
    }

Usage Example

 /**
  * Test WP_Customize_Manager::save_changeset_post().
  *
  * @ticket 30937
  * @covers WP_Customize_Manager::save_changeset_post()
  */
 function test_save_changeset_post_without_theme_activation()
 {
     global $wp_customize;
     wp_set_current_user(self::$admin_user_id);
     $did_action = array('customize_save_validation_before' => did_action('customize_save_validation_before'), 'customize_save' => did_action('customize_save'), 'customize_save_after' => did_action('customize_save_after'));
     $uuid = wp_generate_uuid4();
     $wp_customize = $manager = new WP_Customize_Manager(array('changeset_uuid' => $uuid));
     $wp_customize = $manager;
     $manager->register_controls();
     $manager->set_post_value('blogname', 'Changeset Title');
     $manager->set_post_value('blogdescription', 'Changeset Tagline');
     $pre_saved_data = array('blogname' => array('value' => 'Overridden Changeset Title'), 'blogdescription' => array('custom' => 'something'));
     $date = gmdate('Y') + 1 . '-12-01 00:00:00';
     $r = $manager->save_changeset_post(array('status' => 'auto-draft', 'title' => 'Auto Draft', 'date_gmt' => $date, 'data' => $pre_saved_data));
     $this->assertInternalType('array', $r);
     $this->assertEquals($did_action['customize_save_validation_before'] + 1, did_action('customize_save_validation_before'));
     $post_id = $manager->find_changeset_post_id($uuid);
     $this->assertNotNull($post_id);
     $saved_data = json_decode(get_post($post_id)->post_content, true);
     $this->assertEquals($manager->unsanitized_post_values(), wp_list_pluck($saved_data, 'value'));
     $this->assertEquals($pre_saved_data['blogname']['value'], $saved_data['blogname']['value']);
     $this->assertEquals($pre_saved_data['blogdescription']['custom'], $saved_data['blogdescription']['custom']);
     foreach ($saved_data as $setting_id => $setting_params) {
         $this->assertArrayHasKey('type', $setting_params);
         $this->assertEquals('option', $setting_params['type']);
         $this->assertArrayHasKey('user_id', $setting_params);
         $this->assertEquals(self::$admin_user_id, $setting_params['user_id']);
     }
     $this->assertEquals('Auto Draft', get_post($post_id)->post_title);
     $this->assertEquals('auto-draft', get_post($post_id)->post_status);
     $this->assertEquals($date, get_post($post_id)->post_date_gmt);
     $this->assertNotEquals('Changeset Title', get_option('blogname'));
     $this->assertArrayHasKey('setting_validities', $r);
     // Test saving with invalid settings, ensuring transaction blocked.
     $previous_saved_data = $saved_data;
     $manager->add_setting('foo_unauthorized', array('capability' => 'do_not_allow'));
     $manager->add_setting('baz_illegal', array('validate_callback' => array($this, 'return_illegal_error')));
     $r = $manager->save_changeset_post(array('status' => 'auto-draft', 'data' => array('blogname' => array('value' => 'OK'), 'foo_unauthorized' => array('value' => 'No'), 'bar_unknown' => array('value' => 'No'), 'baz_illegal' => array('value' => 'No'))));
     $this->assertInstanceOf('WP_Error', $r);
     $this->assertEquals('transaction_fail', $r->get_error_code());
     $this->assertInternalType('array', $r->get_error_data());
     $this->assertArrayHasKey('setting_validities', $r->get_error_data());
     $error_data = $r->get_error_data();
     $this->assertArrayHasKey('blogname', $error_data['setting_validities']);
     $this->assertTrue($error_data['setting_validities']['blogname']);
     $this->assertArrayHasKey('foo_unauthorized', $error_data['setting_validities']);
     $this->assertInstanceOf('WP_Error', $error_data['setting_validities']['foo_unauthorized']);
     $this->assertEquals('unauthorized', $error_data['setting_validities']['foo_unauthorized']->get_error_code());
     $this->assertArrayHasKey('bar_unknown', $error_data['setting_validities']);
     $this->assertInstanceOf('WP_Error', $error_data['setting_validities']['bar_unknown']);
     $this->assertEquals('unrecognized', $error_data['setting_validities']['bar_unknown']->get_error_code());
     $this->assertArrayHasKey('baz_illegal', $error_data['setting_validities']);
     $this->assertInstanceOf('WP_Error', $error_data['setting_validities']['baz_illegal']);
     $this->assertEquals('illegal', $error_data['setting_validities']['baz_illegal']->get_error_code());
     // Since transactional, ensure no changes have been made.
     $this->assertEquals($previous_saved_data, json_decode(get_post($post_id)->post_content, true));
     // Attempt a non-transactional/incremental update.
     $wp_customize = $manager = new WP_Customize_Manager(array('changeset_uuid' => $uuid));
     $wp_customize = $manager;
     $manager->register_controls();
     // That is, register settings.
     $r = $manager->save_changeset_post(array('status' => null, 'data' => array('blogname' => array('value' => 'Non-Transactional \\o/ <script>unsanitized</script>'), 'bar_unknown' => array('value' => 'No'))));
     $this->assertInternalType('array', $r);
     $this->assertArrayHasKey('setting_validities', $r);
     $this->assertTrue($r['setting_validities']['blogname']);
     $this->assertInstanceOf('WP_Error', $r['setting_validities']['bar_unknown']);
     $saved_data = json_decode(get_post($post_id)->post_content, true);
     $this->assertNotEquals($previous_saved_data, $saved_data);
     $this->assertEquals('Non-Transactional \\o/ <script>unsanitized</script>', $saved_data['blogname']['value']);
     // Ensure the filter applies.
     $customize_changeset_save_data_call_count = $this->customize_changeset_save_data_call_count;
     add_filter('customize_changeset_save_data', array($this, 'filter_customize_changeset_save_data'), 10, 2);
     $manager->save_changeset_post(array('status' => null, 'data' => array('blogname' => array('value' => 'Filtered'))));
     $this->assertEquals($customize_changeset_save_data_call_count + 1, $this->customize_changeset_save_data_call_count);
     // Publish the changeset: actions will be doubled since also trashed.
     $expected_actions = array('wp_trash_post' => 1, 'clean_post_cache' => 2, 'transition_post_status' => 2, 'publish_to_trash' => 1, 'trash_customize_changeset' => 1, 'edit_post' => 2, 'save_post_customize_changeset' => 2, 'save_post' => 2, 'wp_insert_post' => 2, 'trashed_post' => 1);
     $action_counts = array();
     foreach (array_keys($expected_actions) as $action_name) {
         $action_counts[$action_name] = did_action($action_name);
     }
     $wp_customize = $manager = new WP_Customize_Manager(array('changeset_uuid' => $uuid));
     do_action('customize_register', $wp_customize);
     $manager->add_setting('scratchpad', array('type' => 'option', 'capability' => 'exist'));
     $manager->get_setting('blogname')->capability = 'exist';
     $original_capabilities = wp_list_pluck($manager->settings(), 'capability');
     wp_set_current_user(self::$subscriber_user_id);
     $r = $manager->save_changeset_post(array('status' => 'publish', 'data' => array('blogname' => array('value' => 'Do it live \\o/'), 'scratchpad' => array('value' => '<script>console.info( "HELLO" )</script>'))));
     $this->assertInternalType('array', $r);
     $this->assertEquals('Do it live \\o/', get_option('blogname'));
     $this->assertEquals('trash', get_post_status($post_id));
     // Auto-trashed.
     $this->assertEquals($original_capabilities, wp_list_pluck($manager->settings(), 'capability'));
     $this->assertContains('<script>', get_post($post_id)->post_content);
     $this->assertEquals($manager->changeset_uuid(), get_post($post_id)->post_name, 'Expected that the "__trashed" suffix to not be added.');
     wp_set_current_user(self::$admin_user_id);
     $this->assertEquals('publish', get_post_meta($post_id, '_wp_trash_meta_status', true));
     $this->assertTrue(is_numeric(get_post_meta($post_id, '_wp_trash_meta_time', true)));
     foreach (array_keys($expected_actions) as $action_name) {
         $this->assertEquals($expected_actions[$action_name] + $action_counts[$action_name], did_action($action_name), "Action: {$action_name}");
     }
     // Test revisions.
     add_post_type_support('customize_changeset', 'revisions');
     $uuid = wp_generate_uuid4();
     $wp_customize = $manager = new WP_Customize_Manager(array('changeset_uuid' => $uuid));
     do_action('customize_register', $manager);
     $manager->set_post_value('blogname', 'Hello Surface');
     $manager->save_changeset_post(array('status' => 'auto-draft'));
     $manager->set_post_value('blogname', 'Hello World');
     $manager->save_changeset_post(array('status' => 'draft'));
     $this->assertTrue(wp_revisions_enabled(get_post($manager->changeset_post_id())));
     $manager->set_post_value('blogname', 'Hello Solar System');
     $manager->save_changeset_post(array('status' => 'draft'));
     $manager->set_post_value('blogname', 'Hello Galaxy');
     $manager->save_changeset_post(array('status' => 'draft'));
     $this->assertCount(3, wp_get_post_revisions($manager->changeset_post_id()));
 }
All Usage Examples Of WP_Customize_Manager::save_changeset_post
WP_Customize_Manager