From 4f56c3d18a7236048ed40bfa8c82fb8c88f9b361 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 26 Sep 2023 15:30:34 +0000 Subject: [PATCH] Revisions: framework for storing post meta revisions. Enable the storing of post meta in revisions including autosaves and previews: Add a new argument `revisions_enabled` to the `register_meta` function which enables storing meta in revisions. Add a new `wp_post_revision_meta_keys` filter which developers can use to control which meta is revisioned - it passes an array of the meta keys with revisions enabled as well as the post type. Meta keys with revisions enabled are also stored for autosaves, and are restored when a revision or autosave is restored. In addition, meta values are now stored with the autosave revision used for previews. Changes to meta can now be previewed correctly without overwriting the published meta (see #20299) or passing data as a query variable, as the editor currently does to preview changes to the featured image. Changes to meta with revisions enabled are considered when determining if a new revision should be created. A new revision is created if the meta value has changed since the last revision. Revisions are now saved on the `wp_after_insert_post` hook instead of `post_updated`. The `wp_after_insert_post` action is fired after post meta has been saved by the REST API which enables attaching meta to the revision. To ensure backwards compatibility with existing action uses, `wp_save_post_revision_on_insert` function exits early if plugins have removed the previous `do_action( 'post_updated', 'wp_save_post_revision' )` call. Props: alexkingorg, johnbillion, markjaquith, WraithKenny, kovshenin, azaozz, tv-productions, p51labs, mattheu, mikeschroder, Mamaduka, ellatrix, timothyblynjacobs, jakemgold, bookwyrm, ryanduff, mintindeed, wonderboymusic, sanchothefat, westonruter, spacedmonkey, hellofromTonya, drewapicture, adamsilverstein, swisspiddy. Fixes #20564, #20299. git-svn-id: https://develop.svn.wordpress.org/trunk@56714 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/post.php | 71 +- src/wp-includes/default-filters.php | 13 + src/wp-includes/meta.php | 15 + .../class-wp-rest-autosaves-controller.php | 44 +- .../class-wp-rest-revisions-controller.php | 15 + src/wp-includes/revision.php | 190 ++++- tests/phpunit/tests/meta/registerMeta.php | 36 + tests/phpunit/tests/post/metaRevisions.php | 722 ++++++++++++++++++ .../rest-api/rest-autosaves-controller.php | 8 +- ...est-global-styles-revisions-controller.php | 8 +- .../tests/rest-api/rest-post-meta-fields.php | 367 ++++++++- .../rest-api/rest-revisions-controller.php | 4 +- .../wpRegisterPersistedPreferencesMeta.php | 1 + tests/qunit/fixtures/wp-api-generated.js | 30 + 14 files changed, 1502 insertions(+), 22 deletions(-) create mode 100644 tests/phpunit/tests/post/metaRevisions.php diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 8d7f76b3fe..dd447d9beb 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 adba75fcd7..090ee61d51 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_widgets', 0 ); 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 @@ add_action( 'init', 'wp_register_persisted_preferences_meta' ); // 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 0b9f08544e..96ace4e972 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 d2dc249615..cbc3dc4c90 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 @@ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller { */ $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 @@ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller { * 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 @@ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller { } } + // 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 @@ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller { 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 ); } - // Create the new autosave as a special post revision. - return _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 ) ) { + foreach ( $revisioned_meta_keys as $meta_key ) { + if ( isset( $meta[ $meta_key ] ) ) { + update_metadata( 'post', $revision_id, $meta_key, $meta[ $meta_key ] ); + } + } + } + + 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 16aceb0d74..5501c190c1 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 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { $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 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { ); } + 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 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { $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 d5a5829d7e..d17c79b1b6 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 4b08e4d82b..329321dd9b 100644 --- a/tests/phpunit/tests/meta/registerMeta.php +++ b/tests/phpunit/tests/meta/registerMeta.php @@ -97,6 +97,7 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase { 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -121,6 +122,7 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase { 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -175,6 +177,7 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase { 'sanitize_callback' => array( $this, '_new_sanitize_meta_cb' ), 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -342,6 +345,7 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase { 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -395,6 +399,7 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase { 'sanitize_callback' => null, 'auth_callback' => '__return_true', 'show_in_rest' => false, + 'revisions_enabled' => false, ), ), ), @@ -1081,4 +1086,35 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase { 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 0000000000..74442b53c8 --- /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 fd8f92f7b0..4108d05efc 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 @@ class WP_Test_REST_Autosaves_Controller extends WP_Test_REST_Post_Type_Controlle 'author', 'date', 'date_gmt', + 'id', + 'meta', 'modified', 'modified_gmt', - 'guid', - 'id', 'parent', 'slug', + 'guid', 'title', 'excerpt', 'content', @@ -288,7 +289,7 @@ class WP_Test_REST_Autosaves_Controller extends WP_Test_REST_Post_Type_Controlle $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 @@ class WP_Test_REST_Autosaves_Controller extends WP_Test_REST_Post_Type_Controlle $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 9e580d5f3c..30e5b983ea 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 @@ class WP_REST_Global_Styles_Revisions_Controller_Test extends WP_Test_REST_Contr ), ); - 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 @@ class WP_REST_Global_Styles_Revisions_Controller_Test extends WP_Test_REST_Contr ), ); - 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 @@ class WP_REST_Global_Styles_Revisions_Controller_Test extends WP_Test_REST_Contr ), ); - wp_update_post( $new_styles_post, true, false ); + wp_update_post( $new_styles_post, true ); wp_set_current_user( 0 ); } @@ -326,7 +326,7 @@ class WP_REST_Global_Styles_Revisions_Controller_Test extends WP_Test_REST_Contr '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 11d06b86be..c164c406b0 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 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { 'cpt', array( 'show_in_rest' => true, - 'supports' => array( 'custom-fields' ), + 'supports' => array( 'custom-fields', 'revisions' ), ) ); @@ -157,7 +157,7 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { 'cpt', array( 'show_in_rest' => true, - 'supports' => array( 'custom-fields' ), + 'supports' => array( 'custom-fields', 'revisions' ), ) ); @@ -1376,8 +1376,6 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { * @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 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $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 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { } 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 e0b713c882..74cd040d85 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 @@ class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase 'modified_gmt', 'guid', 'id', + 'meta', 'parent', 'slug', 'title', @@ -335,7 +336,7 @@ class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase $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 @@ class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase $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 9c4725ffbf..8af6ae9b81 100644 --- a/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php +++ b/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php @@ -52,6 +52,7 @@ class Tests_User_WpRegisterPersistedPreferencesMeta extends WP_UnitTestCase { '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 36cab80cc1..8341830452 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": "" } };