diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 8d7f76b3fe063..dd447d9beb1a8 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -1970,11 +1970,12 @@ function wp_create_post_autosave( $post_data ) { * Fires before an autosave is stored. * * @since 4.1.0 + * @since 6.4.0 The `$is_update` parameter was added to indicate if the autosave is being updated or was newly created. * * @param array $new_autosave Post array - the autosave that is about to be saved. + * @param bool $is_update Whether this is an existing autosave. */ - do_action( 'wp_creating_autosave', $new_autosave ); - + do_action( 'wp_creating_autosave', $new_autosave, true ); return wp_update_post( $new_autosave ); } @@ -1982,7 +1983,71 @@ function wp_create_post_autosave( $post_data ) { $post_data = wp_unslash( $post_data ); // Otherwise create the new autosave as a special post revision. - return _wp_put_post_revision( $post_data, true ); + $revision = _wp_put_post_revision( $post_data, true ); + + if ( ! is_wp_error( $revision ) && 0 !== $revision ) { + + /** This action is documented in wp-admin/includes/post.php */ + do_action( 'wp_creating_autosave', get_post( $revision, ARRAY_A ), false ); + } + + return $revision; +} + +/** + * Autosave the revisioned meta fields. + * + * Iterates through the revisioned meta fields and checks each to see if they are set, + * and have a changed value. If so, the meta value is saved and attached to the autosave. + * + * @since 6.4.0 + * + * @param array $new_autosave The new post data being autosaved. + */ +function wp_autosave_post_revisioned_meta_fields( $new_autosave ) { + /* + * The post data arrives as either $_POST['data']['wp_autosave'] or the $_POST + * itself. This sets $posted_data to the correct variable. + * + * Ignoring sanitization to avoid altering meta. Ignoring the nonce check because + * this is hooked on inner core hooks where a valid nonce was already checked. + * + * @phpcs:disable WordPress.Security + */ + $posted_data = isset( $_POST['data']['wp_autosave'] ) ? $_POST['data']['wp_autosave'] : $_POST; + // phpcs:enable + + $post_type = get_post_type( $new_autosave['post_parent'] ); + + /* + * Go thru the revisioned meta keys and save them as part of the autosave, if + * the meta key is part of the posted data, the meta value is not blank and + * the the meta value has changes from the last autosaved value. + */ + foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) { + + if ( + isset( $posted_data[ $meta_key ] ) && + get_post_meta( $new_autosave['ID'], $meta_key, true ) !== wp_unslash( $posted_data[ $meta_key ] ) + ) { + /* + * Use the underlying delete_metadata() and add_metadata() functions + * vs delete_post_meta() and add_post_meta() to make sure we're working + * with the actual revision meta. + */ + delete_metadata( 'post', $new_autosave['ID'], $meta_key ); + + /* + * One last check to ensure meta value not empty(). + */ + if ( ! empty( $posted_data[ $meta_key ] ) ) { + /* + * Add the revisions meta data to the autosave. + */ + add_metadata( 'post', $new_autosave['ID'], $meta_key, $posted_data[ $meta_key ] ); + } + } + } } /** diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index adba75fcd743c..090ee61d5187a 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -411,6 +411,7 @@ add_action( 'plugins_loaded', 'wp_maybe_load_embeds', 0 ); add_action( 'shutdown', 'wp_ob_end_flush_all', 1 ); // Create a revision whenever a post is updated. +add_action( 'wp_after_insert_post', 'wp_save_post_revision_on_insert', 9, 3 ); add_action( 'post_updated', 'wp_save_post_revision', 10, 1 ); add_action( 'publish_post', '_publish_post_hook', 5, 1 ); add_action( 'transition_post_status', '_transition_post_status', 5, 3 ); @@ -719,6 +720,18 @@ // CPT wp_block custom postmeta field. add_action( 'init', 'wp_create_initial_post_meta' ); +// Include revisioned meta when considering whether a post revision has changed. +add_filter( 'wp_save_post_revision_post_has_changed', 'wp_check_revisioned_meta_fields_have_changed', 10, 3 ); + +// Save revisioned post meta immediately after a revision is saved +add_action( '_wp_put_post_revision', 'wp_save_revisioned_meta_fields', 10, 2 ); + +// Include revisioned meta when creating or updating an autosave revision. +add_action( 'wp_creating_autosave', 'wp_autosave_post_revisioned_meta_fields' ); + +// When restoring revisions, also restore revisioned meta. +add_action( 'wp_restore_post_revision', 'wp_restore_post_revision_meta', 10, 2 ); + // Font management. add_action( 'wp_head', 'wp_print_font_faces', 50 ); diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php index 0b9f08544e498..96ace4e9726ba 100644 --- a/src/wp-includes/meta.php +++ b/src/wp-includes/meta.php @@ -1367,6 +1367,7 @@ function sanitize_meta( $meta_key, $meta_value, $object_type, $object_subtype = * @since 4.9.8 The `$object_subtype` argument was added to the arguments array. * @since 5.3.0 Valid meta types expanded to include "array" and "object". * @since 5.5.0 The `$default` argument was added to the arguments array. + * @since 6.4.0 The `$revisions_enabled` argument was added to the arguments array. * * @param string $object_type Type of object metadata is for. Accepts 'post', 'comment', 'term', 'user', * or any other object type with an associated meta table. @@ -1392,6 +1393,8 @@ function sanitize_meta( $meta_key, $meta_value, $object_type, $object_subtype = * support for custom fields for registered meta to be accessible via REST. * When registering complex meta values this argument may optionally be an * array with 'schema' or 'prepare_callback' keys instead of a boolean. + * @type bool $revisions_enabled Whether to enable revisions support for this meta_key. Can only be used when the + * object type is 'post'. * } * @param string|array $deprecated Deprecated. Use `$args` instead. * @return bool True if the meta key was successfully registered in the global array, false if not. @@ -1414,6 +1417,7 @@ function register_meta( $object_type, $meta_key, $args, $deprecated = null ) { 'sanitize_callback' => null, 'auth_callback' => null, 'show_in_rest' => false, + 'revisions_enabled' => false, ); // There used to be individual args for sanitize and auth callbacks. @@ -1460,6 +1464,17 @@ function register_meta( $object_type, $meta_key, $args, $deprecated = null ) { } $object_subtype = ! empty( $args['object_subtype'] ) ? $args['object_subtype'] : ''; + if ( $args['revisions_enabled'] ) { + if ( 'post' !== $object_type ) { + _doing_it_wrong( __FUNCTION__, __( 'Meta keys cannot enable revisions support unless the object type supports revisions.' ), '6.4.0' ); + + return false; + } elseif ( ! empty( $object_subtype ) && ! post_type_supports( $object_subtype, 'revisions' ) ) { + _doing_it_wrong( __FUNCTION__, __( 'Meta keys cannot enable revisions support unless the object subtype supports revisions.' ), '6.4.0' ); + + return false; + } + } // If `auth_callback` is not provided, fall back to `is_protected_meta()`. if ( empty( $args['auth_callback'] ) ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index d2dc2496155f1..fa403e950cfe0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -234,8 +234,8 @@ public function create_item( $request ) { */ $autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true ); } else { - // Non-draft posts: create or update the post autosave. - $autosave_id = $this->create_post_autosave( (array) $prepared_post ); + // Non-draft posts: create or update the post autosave. Pass the meta data. + $autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) ); } if ( is_wp_error( $autosave_id ) ) { @@ -348,11 +348,13 @@ public function get_item_schema() { * From wp-admin/post.php. * * @since 5.0.0 + * @since 6.4.0 The `$meta` parameter was added. * * @param array $post_data Associative array containing the post data. + * @param array $meta Associative array containing the post meta data. * @return mixed The autosave revision ID or WP_Error. */ - public function create_post_autosave( $post_data ) { + public function create_post_autosave( $post_data, array $meta = array() ) { $post_id = (int) $post_data['ID']; $post = get_post( $post_id ); @@ -372,6 +374,21 @@ public function create_post_autosave( $post_data ) { } } + // Check if meta values have changed. + if ( ! empty( $meta ) ) { + $revisioned_meta_keys = wp_post_revision_meta_keys( $post->post_type ); + foreach ( $revisioned_meta_keys as $meta_key ) { + // get_metadata_raw is used to avoid retrieving the default value. + $old_meta = get_metadata_raw( 'post', $post_id, $meta_key, true ); + $new_meta = isset( $meta[ $meta_key ] ) ? $meta[ $meta_key ] : ''; + + if ( $new_meta !== $old_meta ) { + $autosave_is_different = true; + break; + } + } + } + $user_id = get_current_user_id(); // Store one autosave per author. If there is already an autosave, overwrite it. @@ -390,11 +407,26 @@ public function create_post_autosave( $post_data ) { do_action( 'wp_creating_autosave', $new_autosave ); // wp_update_post() expects escaped array. - return wp_update_post( wp_slash( $new_autosave ) ); + $revision_id = wp_update_post( wp_slash( $new_autosave ) ); + } else { + // Create the new autosave as a special post revision. + $revision_id = _wp_put_post_revision( $post_data, true ); + } + + if ( is_wp_error( $revision_id ) || 0 === $revision_id ) { + return $revision_id; + } + + // Attached any passed meta values that have revisions enabled. + if ( ! empty( $meta ) && $revision_id ) { + foreach ( $revisioned_meta_keys as $meta_key ) { + if ( isset( $meta[ $meta_key ] ) ) { + update_metadata( 'post', $revision_id, $meta_key, $meta[ $meta_key ] ); + } + } } - // Create the new autosave as a special post revision. - return _wp_put_post_revision( $post_data, true ); + return $revision_id; } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php index 16aceb0d7441e..5501c190c13ee 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php @@ -24,6 +24,14 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { */ private $parent_post_type; + /** + * Instance of a revision meta fields object. + * + * @since 6.4.0 + * @var WP_REST_Post_Meta_Fields + */ + protected $meta; + /** * Parent controller. * @@ -60,6 +68,7 @@ public function __construct( $parent_post_type ) { $this->rest_base = 'revisions'; $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; + $this->meta = new WP_REST_Post_Meta_Fields( $parent_post_type ); } /** @@ -619,6 +628,10 @@ public function prepare_item_for_response( $item, $request ) { ); } + if ( rest_is_field_included( 'meta', $fields ) ) { + $data['meta'] = $this->meta->get_value( $post->ID, $request ); + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -752,6 +765,8 @@ public function get_item_schema() { $schema['properties']['guid'] = $parent_schema['properties']['guid']; } + $schema['properties']['meta'] = $this->meta->get_field_schema(); + $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); diff --git a/src/wp-includes/revision.php b/src/wp-includes/revision.php index d5a5829d7eef9..d17c79b1b6c67 100644 --- a/src/wp-includes/revision.php +++ b/src/wp-includes/revision.php @@ -95,6 +95,27 @@ function _wp_post_revision_data( $post = array(), $autosave = false ) { return $revision_data; } +/** + * Saves revisions for a post after all changes have been made. + * + * @since 6.4.0 + * + * @param int $post_id The post id that was inserted. + * @param WP_Post $post The post object that was inserted. + * @param bool $update Whether this insert is updating an existing post. + */ +function wp_save_post_revision_on_insert( $post_id, $post, $update ) { + if ( ! $update ) { + return; + } + + if ( ! has_action( 'post_updated', 'wp_save_post_revision' ) ) { + return; + } + + wp_save_post_revision( $post_id ); +} + /** * Creates a revision for the current version of a post. * @@ -111,6 +132,11 @@ function wp_save_post_revision( $post_id ) { return; } + // Prevent saving post revisions if revisions should be saved on wp_after_insert_post. + if ( doing_action( 'post_updated' ) && has_action( 'wp_after_insert_post', 'wp_save_post_revision_on_insert' ) ) { + return; + } + $post = get_post( $post_id ); if ( ! $post ) { @@ -361,15 +387,39 @@ function _wp_put_post_revision( $post = null, $autosave = false ) { * Fires once a revision has been saved. * * @since 2.6.0 + * @since 6.4.0 The post_id parameter was added. * * @param int $revision_id Post revision ID. + * @param int $post_id Post ID. */ - do_action( '_wp_put_post_revision', $revision_id ); + do_action( '_wp_put_post_revision', $revision_id, $post['post_parent'] ); } return $revision_id; } + +/** + * Save the revisioned meta fields. + * + * @since 6.4.0 + * + * @param int $revision_id The ID of the revision to save the meta to. + * @param int $post_id The ID of the post the revision is associated with. + */ +function wp_save_revisioned_meta_fields( $revision_id, $post_id ) { + $post_type = get_post_type( $post_id ); + if ( ! $post_type ) { + return; + } + + foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) { + if ( metadata_exists( 'post', $post_id, $meta_key ) ) { + _wp_copy_post_meta( $post_id, $revision_id, $meta_key ); + } + } +} + /** * Gets a post revision. * @@ -450,6 +500,9 @@ function wp_restore_post_revision( $revision, $fields = null ) { // Update last edit user. update_post_meta( $post_id, '_edit_last', get_current_user_id() ); + // Restore any revisioned meta fields. + wp_restore_post_revision_meta( $post_id, $revision['ID'] ); + /** * Fires after a post revision has been restored. * @@ -463,6 +516,105 @@ function wp_restore_post_revision( $revision, $fields = null ) { return $post_id; } +/** + * Restore the revisioned meta values for a post. + * + * @param int $post_id The ID of the post to restore the meta to. + * @param int $revision_id The ID of the revision to restore the meta from. + * + * @since 6.4.0 + */ +function wp_restore_post_revision_meta( $post_id, $revision_id ) { + $post_type = get_post_type( $post_id ); + if ( ! $post_type ) { + return; + } + + // Restore revisioned meta fields. + foreach ( wp_post_revision_meta_keys( $post_type ) as $meta_key ) { + + // Clear any existing meta. + delete_post_meta( $post_id, $meta_key ); + + _wp_copy_post_meta( $revision_id, $post_id, $meta_key ); + } +} + +/** + * Copy post meta for the given key from one post to another. + * + * @param int $source_post_id Post ID to copy meta value(s) from. + * @param int $target_post_id Post ID to copy meta value(s) to. + * @param string $meta_key Meta key to copy. + * + * @since 6.4.0 + */ +function _wp_copy_post_meta( $source_post_id, $target_post_id, $meta_key ) { + + foreach ( get_post_meta( $source_post_id, $meta_key ) as $meta_value ) { + /** + * We use add_metadata() function vs add_post_meta() here + * to allow for a revision post target OR regular post. + */ + add_metadata( 'post', $target_post_id, $meta_key, wp_slash( $meta_value ) ); + } +} + +/** + * Determine which post meta fields should be revisioned. + * + * @since 6.4.0 + * + * @param string $post_type The post type being revisioned. + * + * @return array An array of meta keys to be revisioned. + */ +function wp_post_revision_meta_keys( $post_type ) { + $registered_meta = array_merge( + get_registered_meta_keys( 'post' ), + get_registered_meta_keys( 'post', $post_type ) + ); + + $wp_revisioned_meta_keys = array(); + + foreach ( $registered_meta as $name => $args ) { + if ( $args['revisions_enabled'] ) { + $wp_revisioned_meta_keys[ $name ] = true; + } + } + + $wp_revisioned_meta_keys = array_keys( $wp_revisioned_meta_keys ); + + /** + * Filter the list of post meta keys to be revisioned. + * + * @since 6.4.0 + * + * @param array $keys An array of meta fields to be revisioned. + * @param string $post_type The post type being revisioned. + */ + return apply_filters( 'wp_post_revision_meta_keys', $wp_revisioned_meta_keys, $post_type ); +} + +/** + * Check whether revisioned post meta fields have changed. + * + * @param bool $post_has_changed Whether the post has changed. + * @param WP_Post $last_revision The last revision post object. + * @param WP_Post $post The post object. + * + * @since 6.4.0 + */ +function wp_check_revisioned_meta_fields_have_changed( $post_has_changed, WP_Post $last_revision, WP_Post $post ) { + foreach ( wp_post_revision_meta_keys( $post->post_type ) as $meta_key ) { + if ( get_post_meta( $post->ID, $meta_key ) !== get_post_meta( $last_revision->ID, $meta_key ) ) { + $post_has_changed = true; + break; + } + } + return $post_has_changed; +} + /** * Deletes a revision. * @@ -728,6 +880,7 @@ function _set_preview( $post ) { add_filter( 'get_the_terms', '_wp_preview_terms_filter', 10, 3 ); add_filter( 'get_post_metadata', '_wp_preview_post_thumbnail_filter', 10, 3 ); + add_filter( 'get_post_metadata', '_wp_preview_meta_filter', 10, 4 ); return $post; } @@ -946,3 +1099,38 @@ function _wp_upgrade_revisions_of_post( $post, $revisions ) { return true; } + +/** + * Filters preview post meta retrieval to get values from the autosave. + * + * Filters revisioned meta keys only. + * + * @since 6.4.0 + * + * @param mixed $value Meta value to filter. + * @param int $object_id Object ID. + * @param string $meta_key Meta key to filter a value for. + * @param bool $single Whether to return a single value. Default false. + * @return mixed Original meta value if the meta key isn't revisioned, the object doesn't exist, + * the post type is a revision or the post ID doesn't match the object ID. + * Otherwise, the revisioned meta value is returned for the preview. + */ +function _wp_preview_meta_filter( $value, $object_id, $meta_key, $single ) { + + $post = get_post(); + if ( + empty( $post ) || + $post->ID !== $object_id || + ! in_array( $meta_key, wp_post_revision_meta_keys( $post->post_type ), true ) || + 'revision' === $post->post_type + ) { + return $value; + } + + $preview = wp_get_post_autosave( $post->ID ); + if ( false === $preview ) { + return $value; + } + + return get_post_meta( $preview->ID, $meta_key, $single ); +} diff --git a/tests/phpunit/tests/meta/registerMeta.php b/tests/phpunit/tests/meta/registerMeta.php index 4b08e4d82b38c..329321dd9bc67 100644 --- a/tests/phpunit/tests/meta/registerMeta.php +++ b/tests/phpunit/tests/meta/registerMeta.php @@ -97,6 +97,7 @@ public function test_register_meta_with_post_object_type_populates_wp_meta_keys( 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -121,6 +122,7 @@ public function test_register_meta_with_term_object_type_populates_wp_meta_keys( 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -175,6 +177,7 @@ public function test_register_meta_with_current_sanitize_callback_populates_wp_m 'sanitize_callback' => array( $this, '_new_sanitize_meta_cb' ), 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -342,6 +345,7 @@ public function test_register_meta_with_subtype_populates_wp_meta_keys( $type, $ 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -395,6 +399,7 @@ public function test_unregister_meta_without_subtype_keeps_subtype_meta_key( $ty 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -1081,4 +1086,35 @@ public function data_get_types_and_subtypes() { array( 'user', 'user' ), ); } + + /** + * Test that attempting to register meta with revisions_enabled set to true on a + * post type that does not have revisions enabled fails and throws a `doing_it_wrong` notice. + * + * @ticket 20564 + */ + public function test_register_meta_with_revisions_enabled_on_post_type_without_revisions() { + $this->setExpectedIncorrectUsage( 'register_meta' ); + + // Set up a custom post type with revisions disabled. + register_post_type( + 'test_post_type', + array( + 'supports' => array( 'title', 'editor' ), + ) + ); + + $meta_key = 'registered_key1'; + $args = array( + 'revisions_enabled' => true, + ); + + $register = register_meta( + 'test_post_type', + $meta_key, + $args + ); + + $this->assertFalse( $register ); + } } diff --git a/tests/phpunit/tests/post/metaRevisions.php b/tests/phpunit/tests/post/metaRevisions.php new file mode 100644 index 0000000000000..74442b53c82ad --- /dev/null +++ b/tests/phpunit/tests/post/metaRevisions.php @@ -0,0 +1,722 @@ +factory->post->create(); + + // And update to store an initial revision. + wp_update_post( + array( + 'post_content' => 'some initial content', + 'ID' => $post_id, + ) + ); + add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_revisioned_keys' ) ); + + // Store a custom meta value, which is not revisioned by default. + update_post_meta( $post_id, 'meta_revision_test', wp_slash( $passed ) ); + $this->assertEquals( $expected, get_post_meta( $post_id, 'meta_revision_test', true ) ); + + // Update the post, storing a revision. + wp_update_post( + array( + 'post_content' => 'some more content', + 'ID' => $post_id, + ) + ); + + // Overwrite. + update_post_meta( $post_id, 'meta_revision_test', 'original' ); + // Update the post, storing a revision. + wp_update_post( + array( + 'post_content' => 'some more content again', + 'ID' => $post_id, + ) + ); + + // Restore the previous revision. + $revisions = (array) wp_get_post_revisions( $post_id ); + + // Go back to load the previous revision. + array_shift( $revisions ); + $last_revision = array_shift( $revisions ); + + // Restore! + wp_restore_post_revision( $last_revision->ID ); + + $this->assertEquals( $expected, get_post_meta( $post_id, 'meta_revision_test', true ) ); + } + + /** + * Provide data for the slashed data tests. + */ + public function slashed_data_provider() { + return array( + array( + 'some\text', + 'some\text', + ), + array( + 'test some\ \\extra \\\slashed \\\\text ', + 'test some\ \\extra \\\slashed \\\\text ', + ), + array( + "This \'is\' an example \n of a \"quoted\" string", + "This \'is\' an example \n of a \"quoted\" string", + ), + array( + 'some unslashed text just to test! % & * ( ) #', + 'some unslashed text just to test! % & * ( ) #', + ), + ); + } + + /** + * Test the revisions system for storage of meta values. + * + * @group revision + */ + public function test_revisions_stores_meta_values() { + /* + * Set Up. + */ + + // Set up a new post. + $post_id = $this->factory->post->create(); + $original_post_id = $post_id; + + // And update to store an initial revision. + wp_update_post( + array( + 'post_content' => 'some initial content', + 'ID' => $post_id, + ) + ); + + // One revision so far. + $revisions = wp_get_post_revisions( $post_id ); + $this->assertCount( 1, $revisions ); + + /* + * First set up a meta value. + */ + + // Store a custom meta value, which is not revisioned by default. + update_post_meta( $post_id, 'meta_revision_test', 'original' ); + + // Update the post, storing a revision. + wp_update_post( + array( + 'post_content' => 'some more content', + 'ID' => $post_id, + ) + ); + + $revisions = wp_get_post_revisions( $post_id ); + $this->assertCount( 2, $revisions ); + + // Next, store some updated meta values for the same key. + update_post_meta( $post_id, 'meta_revision_test', 'update1' ); + + // Save the post, changing content to force a revision. + wp_update_post( + array( + 'post_content' => 'some updated content', + 'ID' => $post_id, + ) + ); + + $revisions = wp_get_post_revisions( $post_id ); + $this->assertCount( 3, $revisions ); + + /* + * Now restore the original revision. + */ + + // Restore the previous revision. + $revisions = (array) wp_get_post_revisions( $post_id ); + + // Go back two to load the previous revision. + array_shift( $revisions ); + $last_revision = array_shift( $revisions ); + + // Restore! + wp_restore_post_revision( $last_revision->ID ); + + wp_update_post( array( 'ID' => $post_id ) ); + $revisions = wp_get_post_revisions( $post_id ); + $this->assertCount( 4, $revisions ); + + /* + * Check the meta values to verify they are NOT revisioned - they are not revisioned by default. + */ + + // Custom post meta should NOT be restored, orignal value should not be restored, value still 'update1'. + $this->assertEquals( 'update1', get_post_meta( $post_id, 'meta_revision_test', true ) ); + + update_post_meta( $post_id, 'meta_revision_test', 'update2' ); + + /* + * Test the revisioning of custom meta when enabled by the wp_post_revision_meta_keys filter. + */ + + // Add the custom field to be revised via the wp_post_revision_meta_keys filter. + add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_revisioned_keys' ) ); + + // Save the post, changing content to force a revision. + wp_update_post( + array( + 'post_content' => 'more updated content', + 'ID' => $post_id, + ) + ); + + $revisions = array_values( wp_get_post_revisions( $post_id ) ); + $this->assertCount( 5, $revisions ); + $this->assertEquals( 'update2', get_post_meta( $revisions[0]->ID, 'meta_revision_test', true ) ); + + // Store custom meta values, which should now be revisioned. + update_post_meta( $post_id, 'meta_revision_test', 'update3' ); + + /* + * Save the post again, custom meta should now be revisioned. + * + * Note that a revision is saved even though there is no change + * in post content, because the revisioned post_meta has changed. + */ + wp_update_post( + array( + 'ID' => $post_id, + ) + ); + + // This revision contains the existing post meta ('update3'). + $revisions = wp_get_post_revisions( $post_id ); + $this->assertCount( 6, $revisions ); + + // Verify that previous post meta is set. + $this->assertEquals( 'update3', get_post_meta( $post_id, 'meta_revision_test', true ) ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + + // Go back two to load the previous revision. + array_shift( $revisions ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + + /* + * Verify that previous post meta is restored. + */ + $this->assertEquals( 'update2', get_post_meta( $post_id, 'meta_revision_test', true ) ); + + // Try storing a blank meta. + update_post_meta( $post_id, 'meta_revision_test', '' ); + wp_update_post( + array( + 'ID' => $post_id, + ) + ); + + update_post_meta( $post_id, 'meta_revision_test', 'update 4' ); + wp_update_post( + array( + 'ID' => $post_id, + ) + ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + array_shift( $revisions ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + + /* + * Verify that previous blank post meta is restored. + */ + $this->assertEquals( '', get_post_meta( $post_id, 'meta_revision_test', true ) ); + + /* + * Test not tracking a key - remove the key from the revisioned meta. + */ + remove_all_filters( 'wp_post_revision_meta_keys' ); + + // Meta should no longer be revisioned. + update_post_meta( $post_id, 'meta_revision_test', 'update 5' ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'changed content', + ) + ); + update_post_meta( $post_id, 'meta_revision_test', 'update 6' ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'go updated content', + ) + ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + array_shift( $revisions ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + + /* + * Verify that previous post meta is NOT restored. + */ + $this->assertEquals( 'update 6', get_post_meta( $post_id, 'meta_revision_test', true ) ); + + // Add the custom field to be revised via the wp_post_revision_meta_keys filter. + add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_revisioned_keys' ) ); + + /* + * Test the revisioning of multiple meta keys. + */ + + // Add three values for meta. + update_post_meta( $post_id, 'meta_revision_test', 'update 7' ); + add_post_meta( $post_id, 'meta_revision_test', 'update 7 number 2' ); + add_post_meta( $post_id, 'meta_revision_test', 'update 7 number 3' ); + wp_update_post( array( 'ID' => $post_id ) ); + + // Update all three values. + update_post_meta( $post_id, 'meta_revision_test', 'update 8', 'update 7' ); + update_post_meta( $post_id, 'meta_revision_test', 'update 8 number 2', 'update 7 number 2' ); + update_post_meta( $post_id, 'meta_revision_test', 'update 8 number 3', 'update 7 number 3' ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + + /* + * Verify that multiple metas stored correctly. + */ + $this->assertEquals( array( 'update 7', 'update 7 number 2', 'update 7 number 3' ), get_post_meta( $post_id, 'meta_revision_test' ) ); + + /* + * Test the revisioning of a multidimensional array. + */ + $test_array = array( + 'a' => array( + '1', + '2', + '3', + ), + 'b' => 'ok', + 'c' => array( + 'multi' => array( + 'a', + 'b', + 'c', + ), + 'not' => 'ok', + ), + ); + + // Clear any old value. + delete_post_meta( $post_id, 'meta_revision_test' ); + + // Set the test meta to the array. + update_post_meta( $post_id, 'meta_revision_test', $test_array ); + + // Update to save. + wp_update_post( array( 'ID' => $post_id ) ); + + // Set the test meta blank. + update_post_meta( $post_id, 'meta_revision_test', '' ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + + /* + * Verify multidimensional array stored correctly. + */ + $stored_array = get_post_meta( $post_id, 'meta_revision_test' ); + $this->assertEquals( $test_array, $stored_array[0] ); + /* + + * Test multiple revisions on the same key. + */ + + // Set the test meta to the array. + add_post_meta( $post_id, 'meta_multiples_test', 'test1' ); + add_post_meta( $post_id, 'meta_multiples_test', 'test2' ); + add_post_meta( $post_id, 'meta_multiples_test', 'test3' ); + + // Update to save. + wp_update_post( array( 'ID' => $post_id ) ); + + $stored_array = get_post_meta( $post_id, 'meta_multiples_test' ); + $expect = array( 'test1', 'test2', 'test3' ); + + $this->assertEquals( $expect, $stored_array ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + + $stored_array = get_post_meta( $post_id, 'meta_multiples_test' ); + $expect = array( 'test1', 'test2', 'test3' ); + + $this->assertEquals( $expect, $stored_array ); + + // Cleanup! + wp_delete_post( $original_post_id ); + } + + /** + * Verify that only existing meta is revisioned. + */ + public function only_existing_meta_is_revisioned() { + add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_revisioned_keys' ) ); + + // Set up a new post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'initial content', + ) + ); + + // Revision v1. + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'updated content v1', + ) + ); + + $this->assertPostNotHasMetaKey( $post_id, 'foo' ); + $this->assertPostNotHasMetaKey( $post_id, 'bar' ); + + $revisions = wp_get_post_revisions( $post_id ); + $revision = array_shift( $revisions ); + $this->assertEmpty( get_metadata( 'post', $revision->ID ) ); + + // Revision v2. + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'updated content v2', + 'meta_input' => array( + 'foo' => 'foo v2', + ), + ) + ); + + $this->assertPostHasMetaKey( $post_id, 'foo' ); + $this->assertPostNotHasMetaKey( $post_id, 'bar' ); + $this->assertPostNotHasMetaKey( $post_id, 'meta_revision_test' ); + + $revisions = wp_get_post_revisions( $post_id ); + $revision = array_shift( $revisions ); + $this->assertPostHasMetaKey( $revision->ID, 'foo' ); + $this->assertPostNotHasMetaKey( $revision->ID, 'bar' ); + $this->assertPostNotHasMetaKey( $revision->ID, 'meta_revision_test' ); + } + + /** + * Verify that blank strings are revisioned correctly. + */ + public function blank_meta_is_revisioned() { + + add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_revisioned_keys' ) ); + + // Set up a new post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'initial content', + 'meta_input' => array( + 'foo' => 'foo', + ), + ) + ); + + // Set the test meta to an empty string. + update_post_meta( $post_id, 'foo', '' ); + + // Update to save. + wp_update_post( array( 'ID' => $post_id ) ); + + $stored_array = get_post_meta( $post_id, 'meta_multiples_test' ); + $expect = array( 'test1', 'test2', 'test3' ); + + $this->assertEquals( $expect, $stored_array ); + + // Restore the previous revision. + $revisions = wp_get_post_revisions( $post_id ); + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + $stored_data = get_post_meta( $post_id, 'foo' ); + $this->assertEquals( '', $stored_data[0] ); + } + + /** + * Test revisioning of meta with a default value. + */ + public function test_revisionining_of_meta_with_default_value() { + + // Add a meta field to revision that includes a default value. + register_post_meta( + 'post', + 'meta_revision_test', + array( + 'single' => true, + 'default' => 'default value', + 'revisions_enabled' => true, + ) + ); + + // Set up a new post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'initial content', + 'meta_input' => array( + 'meta_revision_test' => 'foo', + ), + ) + ); + + // Set the test meta to an empty string. + update_post_meta( $post_id, 'meta_revision_test', '' ); + + // Update to save. + wp_update_post( array( 'ID' => $post_id ) ); + + // Check that the meta is blank. + $stored_data = get_post_meta( $post_id, 'meta_revision_test', true ); + $this->assertEquals( '', $stored_data ); + + // Also verify that the latest revision has blank stored for the meta. + $revisions = wp_get_post_revisions( $post_id ); + $last_revision = array_shift( $revisions ); + $stored_data = get_post_meta( $last_revision->ID, 'meta_revision_test', true ); + $this->assertEquals( '', $stored_data ); + + // Delete the meta. + delete_post_meta( $post_id, 'meta_revision_test' ); + + // Update to save. + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'content update 1', + ) + ); + + // Check that the default meta value is returned. + $this->assertEquals( 'default value', get_post_meta( $post_id, 'meta_revision_test', true ) ); + + // Also verify that the latest revision has the default value returned for the meta. + $revisions = wp_get_post_revisions( $post_id ); + $last_revision = array_shift( $revisions ); + + // No ,eta data should be stored in the revision. + $this->assertEquals( array(), get_post_meta( $last_revision->ID ) ); + + // Set the test meta again. + update_post_meta( $post_id, 'meta_revision_test', 'test' ); + + // Update to save. + wp_update_post( array( 'ID' => $post_id ) ); + + // Now restore the previous revision. + wp_restore_post_revision( $last_revision->ID ); + + // Verify the default meta value is still returned. + $this->assertEquals( 'default value', get_post_meta( $post_id, 'meta_revision_test', true ) ); + } + + /** + * @dataProvider data_register_post_meta_supports_revisions + */ + public function test_register_post_meta_supports_revisions( $post_type, $meta_key, $args, $expected_is_revisioned ) { + register_post_meta( $post_type, $meta_key, $args ); + + // Set up a new post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'initial content', + 'post_type' => $post_type, + 'meta_input' => array( + $meta_key => 'foo', + ), + ) + ); + + // Update the post meta and post to save. + update_post_meta( $post_id, $meta_key, 'bar' ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_title' => 'updated title', + ) + ); + + // Check the last revision for the post to see if the meta key was revisioned + $revisions = wp_get_post_revisions( $post_id ); + $revision = array_shift( $revisions ); + $revisioned_meta = get_post_meta( $revision->ID, $meta_key, true ); + $this->assertEquals( $expected_is_revisioned, 'bar' === $revisioned_meta ); + + // Reset global so subsequent data tests do not get polluted. + $GLOBALS['wp_meta_keys'] = array(); + } + + public function data_register_post_meta_supports_revisions() { + return array( + array( 'post', 'registered_key1', array( 'single' => true ), false ), + array( + 'post', + 'registered_key1', + array( + 'single' => true, + 'revisions_enabled' => true, + ), + true, + ), + array( 'page', 'registered_key2', array( 'revisions_enabled' => false ), false ), + array( 'page', 'registered_key2', array( 'revisions_enabled' => true ), true ), + array( '', 'registered_key3', array( 'revisions_enabled' => false ), false ), + array( '', 'registered_key3', array( 'revisions_enabled' => true ), true ), + ); + } + + /** + * Assert the a post has a meta key. + * + * @param int $post_id The ID of the post to check. + * @param string $meta_key The meta key to check for. + */ + protected function assertPostHasMetaKey( $post_id, $meta_key ) { + $this->assertArrayHasKey( $meta_key, get_metadata( 'post', $post_id ) ); + } + + /** + * Assert that post does not have a meta key. + * + * @param int $post_id The ID of the post to check. + * @param string $meta_key The meta key to check for. + */ + protected function assertPostNotHasMetaKey( $post_id, $meta_key ) { + $this->assertArrayNotHasKey( $meta_key, get_metadata( 'post', $post_id ) ); + } + + /** + * Test post meta revisioning with a custom post type, as well as the "page" post type. + * + * @dataProvider page_post_type_data_provider + */ + public function test_revisions_stores_meta_values_page_and_cpt( $passed, $expected, $post_type, $supports_revisions = false ) { + + // If the post type doesn't exist, create it, potentially supporting revisions. + if ( ! post_type_exists( $post_type ) ) { + register_post_type( + $post_type, + array( + 'public' => true, + 'supports' => $supports_revisions ? array( 'revisions' ) : array(), + ) + ); + } + + // Create a test post. + $page_id = $this->factory->post->create( + array( + 'post_type' => $post_type, + 'post_content' => 'some initial content', + ) + ); + + // Add the revisioning filter. + add_filter( 'wp_post_revision_meta_keys', array( $this, 'add_revisioned_keys' ) ); + + // Test revisioning. + update_post_meta( $page_id, 'meta_revision_test', wp_slash( $passed ) ); + + // Update the post, storing a revision. + wp_update_post( + array( + 'post_content' => 'some more content', + 'ID' => $page_id, + ) + ); + + // Retrieve the created revision. + $revisions = (array) wp_get_post_revisions( $page_id ); + + if ( $expected ) { + // Go back to load the previous revision. + $last_revision = array_shift( $revisions ); + wp_restore_post_revision( $last_revision->ID ); + $this->assertEquals( $expected, get_post_meta( $page_id, 'meta_revision_test', true ) ); + } else { + $this->assertEmpty( $revisions ); + } + } + + /** + * Provide data for the page post type tests. + */ + public function page_post_type_data_provider() { + return array( + array( + 'Test string', + 'Test string', + 'page', + ), + array( + 'Test string', + false, + 'custom_type', + ), + array( + 'Test string', + 'Test string', + 'custom_type', + true, + ), + ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index fd8f92f7b0ae9..4108d05efc538 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -215,12 +215,13 @@ public function test_get_item() { 'author', 'date', 'date_gmt', + 'id', + 'meta', 'modified', 'modified_gmt', - 'guid', - 'id', 'parent', 'slug', + 'guid', 'title', 'excerpt', 'content', @@ -288,7 +289,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 13, $properties ); + $this->assertCount( 14, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'content', $properties ); $this->assertArrayHasKey( 'date', $properties ); @@ -302,6 +303,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'slug', $properties ); $this->assertArrayHasKey( 'title', $properties ); $this->assertArrayHasKey( 'preview_link', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); } public function test_create_item() { diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php index 9e580d5f3c978..30e5b983eae9c 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php @@ -132,7 +132,7 @@ public static function wpSetupBeforeClass( $factory ) { ), ); - wp_update_post( $new_styles_post, true, false ); + wp_update_post( $new_styles_post, true ); $new_styles_post = array( 'ID' => self::$global_styles_id, @@ -162,7 +162,7 @@ public static function wpSetupBeforeClass( $factory ) { ), ); - wp_update_post( $new_styles_post, true, false ); + wp_update_post( $new_styles_post, true ); $new_styles_post = array( 'ID' => self::$global_styles_id, @@ -192,7 +192,7 @@ public static function wpSetupBeforeClass( $factory ) { ), ); - wp_update_post( $new_styles_post, true, false ); + wp_update_post( $new_styles_post, true ); wp_set_current_user( 0 ); } @@ -326,7 +326,7 @@ public function test_get_items_eligible_roles() { 'post_content' => wp_json_encode( $config ), ); - wp_update_post( $updated_styles_post, true, false ); + wp_update_post( $updated_styles_post, true ); $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); diff --git a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php index 11d06b86bef74..c164c406b0159 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -17,7 +17,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'cpt', array( 'show_in_rest' => true, - 'supports' => array( 'custom-fields' ), + 'supports' => array( 'custom-fields', 'revisions' ), ) ); @@ -157,7 +157,7 @@ public function set_up() { 'cpt', array( 'show_in_rest' => true, - 'supports' => array( 'custom-fields' ), + 'supports' => array( 'custom-fields', 'revisions' ), ) ); @@ -1376,8 +1376,6 @@ public function data_set_subtype_meta_value() { * @dataProvider data_update_value_return_success_with_same_value */ public function test_update_value_return_success_with_same_value( $meta_key, $meta_value ) { - add_post_meta( self::$post_id, $meta_key, $meta_value ); - $this->grant_write_permission(); $data = array( @@ -1392,6 +1390,12 @@ public function test_update_value_return_success_with_same_value( $meta_key, $me $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); + + // Verify the returned meta value is correct. + $data = $response->get_data(); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertArrayHasKey( $meta_key, $data['meta'] ); + $this->assertSame( $meta_value, $data['meta'][ $meta_key ] ); } public function data_update_value_return_success_with_same_value() { @@ -3112,4 +3116,359 @@ public function error_delete_query( $query ) { } return $query; } + + + /** + * Test that single post meta is revisioned when saving to the posts REST API endpoint. + * + * @ticket 20564 + */ + public function test_revisioned_single_post_meta_with_posts_endpoint() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'foo', + array( + 'single' => true, + 'show_in_rest' => true, + 'revisions_enabled' => true, + ) + ); + + $post_id = self::$post_id; + + // Update the post, saving the meta. + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1', + 'meta' => array( + 'foo' => 'bar', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id = array_shift( $revisions )->ID; + + // @todo Ensure the revisions endpoint returns the correct meta values + // Check that the revisions endpoint returns the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'bar', $response->get_data()['meta']['foo'] ); + + // Check that the post meta is set correctly. + $this->assertSame( 'bar', get_post_meta( $revision_id, 'foo', true ) ); + + // Create two more revisions with different meta values for the foo key. + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 2', + 'meta' => array( + 'foo' => 'baz', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_2 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_2 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'baz', $response->get_data()['meta']['foo'] ); + + // Check that the post meta is set correctly. + $this->assertSame( 'baz', get_post_meta( $revision_id_2, 'foo', true ) ); + + // One more revision! + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 3', + 'meta' => array( + 'foo' => 'qux', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_3 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_3 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'qux', $response->get_data()['meta']['foo'] ); + + // Check that the post meta is set correctly. + $this->assertSame( 'qux', get_post_meta( $revision_id_3, 'foo', true ) ); + + // Restore Revision 3 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_3 ); + $this->assertSame( 'qux', get_post_meta( $post_id, 'foo', true ) ); + + // Restore Revision 2 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_2 ); + $this->assertSame( 'baz', get_post_meta( $post_id, 'foo', true ) ); + } + + /** + * Test that multi-post meta is revisioned when saving to the posts REST API endpoint. + * + * @ticket 20564 + */ + public function test_revisioned_multiple_post_meta_with_posts_endpoint() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'foo', + array( + 'single' => false, + 'show_in_rest' => true, + 'revisions_enabled' => true, + ) + ); + + $post_id = self::$post_id; + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1', + 'meta' => array( + 'foo' => array( + 'bar', + 'bat', + 'baz', + ), + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Log the current post meta. + $meta = get_post_meta( $post_id ); + + // Update the post. + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1 update', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_1 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_1 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + array( 'bar', 'bat', 'baz' ), + $response->get_data()['meta']['foo'] + ); + $this->assertSame( + array( 'bar', 'bat', 'baz' ), + get_post_meta( $revision_id_1, 'foo' ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 2', + 'meta' => array( + 'foo' => array( + 'car', + 'cat', + ), + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_2 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_2 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + array( 'car', 'cat' ), + $response->get_data()['meta']['foo'] + ); + $this->assertSame( array( 'car', 'cat' ), get_post_meta( $revision_id_2, 'foo' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 3', + 'meta' => array( + 'foo' => null, + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_3 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_3 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + array(), + $response->get_data()['meta']['foo'] + ); + $this->assertSame( array(), get_post_meta( $revision_id_3, 'foo' ) ); + + // Restore Revision 3 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_3 ); + $this->assertSame( array(), get_post_meta( $post_id, 'foo' ) ); + + // Restore Revision 2 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_2 ); + $this->assertSame( array( 'car', 'cat' ), get_post_meta( $post_id, 'foo' ) ); + } + + /** + * Test post meta revisions with a custom post type and the page post type. + * + * @group revision + * @dataProvider test_revisioned_single_post_meta_with_posts_endpoint_page_and_cpt_data_provider + */ + public function test_revisioned_single_post_meta_with_posts_endpoint_page_and_cpt( $passed, $expected, $post_type ) { + + $this->grant_write_permission(); + + // Create the custom meta. + register_post_meta( + $post_type, + 'foo', + array( + 'show_in_rest' => true, + 'revisions_enabled' => true, + 'single' => true, + 'type' => 'string', + ) + ); + + // Set up a new post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'initial content', + 'post_type' => $post_type, + 'meta_input' => array( + 'foo' => 'foo', + ), + ) + ); + + $plural_mapping = array( + 'page' => 'pages', + 'cpt' => 'cpt', + ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/%s', $plural_mapping[ $post_type ] ) ); + + $response = rest_get_server()->dispatch( $request ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/%s/%d', $plural_mapping[ $post_type ], $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1', + 'meta' => array( + 'foo' => $passed, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Update the post. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/%s/%d', $plural_mapping[ $post_type ], $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1 update', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + + $revision_id_1 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/%s/%d/revisions/%d', $plural_mapping[ $post_type ], $post_id, $revision_id_1 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + $passed, + $response->get_data()['meta']['foo'] + ); + + $this->assertSame( + array( $passed ), + get_post_meta( $revision_id_1, 'foo' ) + ); + + unregister_post_meta( $post_type, 'foo' ); + wp_delete_post( $post_id, true ); + } + + /** + * Provide data for the meta revision checks. + */ + public function test_revisioned_single_post_meta_with_posts_endpoint_page_and_cpt_data_provider() { + return array( + array( + 'Test string', + 'Test string', + 'cpt', + ), + array( + 'Test string', + 'Test string', + 'page', + ), + array( + 'Test string', + false, + 'cpt', + ), + + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index e0b713c882bc7..74cd040d85f68 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -179,6 +179,7 @@ public function test_get_item() { 'modified_gmt', 'guid', 'id', + 'meta', 'parent', 'slug', 'title', @@ -335,7 +336,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 12, $properties ); + $this->assertCount( 13, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'content', $properties ); $this->assertArrayHasKey( 'date', $properties ); @@ -348,6 +349,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'parent', $properties ); $this->assertArrayHasKey( 'slug', $properties ); $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); } public function test_create_item() { diff --git a/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php b/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php index 9c4725ffbfc9e..8af6ae9b81533 100644 --- a/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php +++ b/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php @@ -52,6 +52,7 @@ public function test_should_register_persisted_preferences_meta() { 'additionalProperties' => true, ), ), + 'revisions_enabled' => false, ), $wp_meta_keys['user'][''][ $meta_key ], 'The registered metadata did not have the expected structure' diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 36cab80cc18e0..8341830452a1f 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11782,6 +11782,9 @@ mockedApiResponse.postRevisions = [ "excerpt": { "rendered": "" }, + "meta": { + "meta_key": "" + }, "_links": { "parent": [ { @@ -11811,6 +11814,9 @@ mockedApiResponse.postRevisions = [ "excerpt": { "rendered": "

REST API Client Fixture: Post

\n" }, + "meta": { + "meta_key": "" + }, "_links": { "parent": [ { @@ -11841,6 +11847,9 @@ mockedApiResponse.revision = { }, "excerpt": { "rendered": "

REST API Client Fixture: Post

\n" + }, + "meta": { + "meta_key": "" } }; @@ -11866,6 +11875,9 @@ mockedApiResponse.postAutosaves = [ "excerpt": { "rendered": "" }, + "meta": { + "meta_key": "" + }, "_links": { "parent": [ { @@ -11896,6 +11908,9 @@ mockedApiResponse.autosave = { }, "excerpt": { "rendered": "" + }, + "meta": { + "meta_key": "" } }; @@ -12042,6 +12057,9 @@ mockedApiResponse.pageRevisions = [ "excerpt": { "rendered": "" }, + "meta": { + "meta_key": "" + }, "_links": { "parent": [ { @@ -12071,6 +12089,9 @@ mockedApiResponse.pageRevisions = [ "excerpt": { "rendered": "

REST API Client Fixture: Page

\n" }, + "meta": { + "meta_key": "" + }, "_links": { "parent": [ { @@ -12101,6 +12122,9 @@ mockedApiResponse.pageRevision = { }, "excerpt": { "rendered": "

REST API Client Fixture: Page

\n" + }, + "meta": { + "meta_key": "" } }; @@ -12126,6 +12150,9 @@ mockedApiResponse.pageAutosaves = [ "excerpt": { "rendered": "" }, + "meta": { + "meta_key": "" + }, "_links": { "parent": [ { @@ -12156,6 +12183,9 @@ mockedApiResponse.pageAutosave = { }, "excerpt": { "rendered": "" + }, + "meta": { + "meta_key": "" } };