Make WordPress Core

Changeset 58831


Ignore:
Timestamp:
07/29/2024 06:47:21 PM (2 months ago)
Author:
dmsnell
Message:

REST API, Meta: Store updates in database when they are equal to the defaults.

This patch fixes an oversight from when default metadata values were introduced
in #43941 in WordPress 5.5: metadata updates should persist in the database
even if they match the registered default value (because the default values
can change over time).

Previously, the REST API code was comparing updated values against the value
returned by the default-aware get_metadata() method. This meant that if no
value existed in the database, and the default value was supplied to the update,
WordPress would think that the updated value was already persisted and skip
the database call.

Now, the get_metadata_raw() method is called for comparing whether or not
a database update is required, fixing the bug.

In this patch both issues are resolved.

Developed in https://github.com/wordpress/wordpress-develop/pull/6782
Discussed in https://core.trac.wordpress.org/ticket/55600

Follow-up to [48402].

Props: dmsnell, kraftner, ramon-fincken.
Fixes #55600.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php

    r57611 r58831  
    269269     *
    270270     * @since 4.7.0
     271     * @since 6.7.0 Stores values into DB even if provided registered default value.
    271272     *
    272273     * @param int    $object_id Object ID to update.
     
    291292        }
    292293
    293         $current_values = get_metadata( $meta_type, $object_id, $meta_key, false );
     294        $current_values = get_metadata_raw( $meta_type, $object_id, $meta_key, false );
    294295        $subtype        = get_object_subtype( $meta_type, $object_id );
    295296
     
    368369     *
    369370     * @since 4.7.0
     371     * @since 6.7.0 Stores values into DB even if provided registered default value.
    370372     *
    371373     * @param int    $object_id Object ID to update.
     
    379381
    380382        // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
    381         $old_value = get_metadata( $meta_type, $object_id, $meta_key );
     383        $old_value = get_metadata_raw( $meta_type, $object_id, $meta_key );
    382384        $subtype   = get_object_subtype( $meta_type, $object_id );
    383385
  • trunk/tests/phpunit/tests/rest-api/rest-post-meta-fields.php

    r57611 r58831  
    30973097
    30983098    /**
     3099     * Ensures that REST API calls with post meta containing the default value for the
     3100     * registered meta field stores the default value into the database.
     3101     *
     3102     * When the default value isn't persisted in the database, a read of the post meta
     3103     * at some point in the future might return a different value if the code setting the
     3104     * default changed. This ensures that once a value is intentionally saved into the
     3105     * database that it will remain durably in future reads.
     3106     *
     3107     * @ticket 55600
     3108     *
     3109     * @dataProvider data_scalar_default_values
     3110     *
     3111     * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
     3112     * @param mixed  $default_value     Appropriate default value for given type.
     3113     * @param mixed  $alternative_value Ignored in this test.
     3114     */
     3115    public function test_scalar_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
     3116        $this->grant_write_permission();
     3117
     3118        $meta_key_single = "with_{$type}_default";
     3119
     3120        register_post_meta(
     3121            'post',
     3122            $meta_key_single,
     3123            array(
     3124                'type'         => $type,
     3125                'single'       => true,
     3126                'show_in_rest' => true,
     3127                'default'      => $default_value,
     3128            )
     3129        );
     3130
     3131        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3132        $request->set_body_params(
     3133            array(
     3134                'meta' => array(
     3135                    $meta_key_single => $default_value,
     3136                ),
     3137            )
     3138        );
     3139
     3140        $response = rest_get_server()->dispatch( $request );
     3141        $this->assertSame(
     3142            200,
     3143            $response->get_status(),
     3144            "API call should have returned successfully but didn't: check test setup."
     3145        );
     3146
     3147        $this->assertSame(
     3148            array( (string) $default_value ),
     3149            get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ),
     3150            'Should have stored a single meta value with string-cast version of default value.'
     3151        );
     3152    }
     3153
     3154    /**
     3155     * Ensures that REST API calls with multi post meta values (containing the default)
     3156     * for the registered meta field stores the default value into the database.
     3157     *
     3158     * When the default value isn't persisted in the database, a read of the post meta
     3159     * at some point in the future might return a different value if the code setting the
     3160     * default changed. This ensures that once a value is intentionally saved into the
     3161     * database that it will remain durably in future reads.
     3162     *
     3163     * Further, the total count of stored values may be wrong if the default value
     3164     * is culled from the results of a "multi" read.
     3165     *
     3166     * @ticket 55600
     3167     *
     3168     * @dataProvider data_scalar_default_values
     3169     *
     3170     * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
     3171     * @param mixed  $default_value     Appropriate default value for given type.
     3172     * @param mixed  $alternative_value Appropriate value for given type that doesn't match the default value.
     3173     */
     3174    public function test_scalar_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
     3175        $this->grant_write_permission();
     3176
     3177        $meta_key_multiple = "with_multi_{$type}_default";
     3178
     3179        // Register non-singular post meta for type.
     3180        register_post_meta(
     3181            'post',
     3182            $meta_key_multiple,
     3183            array(
     3184                'type'         => $type,
     3185                'single'       => false,
     3186                'show_in_rest' => true,
     3187                'default'      => $default_value,
     3188            )
     3189        );
     3190
     3191        // Write the default value as the sole value.
     3192        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3193        $request->set_body_params(
     3194            array(
     3195                'meta' => array(
     3196                    $meta_key_multiple => array( $default_value ),
     3197                ),
     3198            )
     3199        );
     3200
     3201        $response = rest_get_server()->dispatch( $request );
     3202        $this->assertSame(
     3203            200,
     3204            $response->get_status(),
     3205            "API call should have returned successfully but didn't: check test setup."
     3206        );
     3207
     3208        $this->assertSame(
     3209            array( (string) $default_value ),
     3210            get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
     3211            'Should have stored a single meta value with string-cast version of default value.'
     3212        );
     3213
     3214        // Write multiple values, including the default, to ensure it remains.
     3215        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3216        $request->set_body_params(
     3217            array(
     3218                'meta' => array(
     3219                    $meta_key_multiple => array(
     3220                        $default_value,
     3221                        $alternative_value,
     3222                    ),
     3223                ),
     3224            )
     3225        );
     3226
     3227        $response = rest_get_server()->dispatch( $request );
     3228        $this->assertSame(
     3229            200,
     3230            $response->get_status(),
     3231            "API call should have returned successfully but didn't: check test setup."
     3232        );
     3233
     3234        $this->assertSame(
     3235            array( (string) $default_value, (string) $alternative_value ),
     3236            get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
     3237            'Should have stored both the default and non-default string-cast values.'
     3238        );
     3239    }
     3240
     3241    /**
     3242     * Ensures that REST API calls with post meta containing an object as the default
     3243     * value for the registered meta field stores the default value into the database.
     3244     *
     3245     * When the default value isn't persisted in the database, a read of the post meta
     3246     * at some point in the future might return a different value if the code setting the
     3247     * default changed. This ensures that once a value is intentionally saved into the
     3248     * database that it will remain durably in future reads.
     3249     *
     3250     * @ticket 55600
     3251     *
     3252     * @dataProvider data_scalar_default_values
     3253     *
     3254     * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
     3255     * @param mixed  $default_value     Appropriate default value for given type.
     3256     * @param mixed  $alternative_value Ignored in this test.
     3257     */
     3258    public function test_object_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
     3259        $this->grant_write_permission();
     3260
     3261        $meta_key_single = "with_{$type}_default";
     3262
     3263        // Register singular post meta for type.
     3264        register_post_meta(
     3265            'post',
     3266            $meta_key_single,
     3267            array(
     3268                'type'         => 'object',
     3269                'single'       => true,
     3270                'show_in_rest' => array(
     3271                    'schema' => array(
     3272                        'type'       => 'object',
     3273                        'properties' => array(
     3274                            $type => array( 'type' => $type ),
     3275                        ),
     3276                    ),
     3277                ),
     3278                'default'      => (object) array( $type => $default_value ),
     3279            )
     3280        );
     3281
     3282        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3283        $request->set_body_params(
     3284            array(
     3285                'meta' => array(
     3286                    $meta_key_single => (object) array( $type => $default_value ),
     3287                ),
     3288            )
     3289        );
     3290
     3291        $response = rest_get_server()->dispatch( $request );
     3292        $this->assertSame(
     3293            200,
     3294            $response->get_status(),
     3295            "API call should have returned successfully but didn't: check test setup."
     3296        );
     3297
     3298        // Objects stored into the database are read back as arrays.
     3299        $this->assertSame(
     3300            array( array( $type => $default_value ) ),
     3301            get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ),
     3302            'Should have stored a single meta value with an object representing the default value.'
     3303        );
     3304    }
     3305
     3306    /**
     3307     * Ensures that REST API calls with multi post meta values (containing an object as
     3308     * the default) for the registered meta field stores the default value into the database.
     3309     *
     3310     * When the default value isn't persisted in the database, a read of the post meta
     3311     * at some point in the future might return a different value if the code setting the
     3312     * default changed. This ensures that once a value is intentionally saved into the
     3313     * database that it will remain durably in future reads.
     3314     *
     3315     * Further, the total count of stored values may be wrong if the default value
     3316     * is culled from the results of a "multi" read.
     3317     *
     3318     * @ticket 55600
     3319     *
     3320     * @dataProvider data_scalar_default_values
     3321     *
     3322     * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
     3323     * @param mixed  $default_value     Appropriate default value for given type.
     3324     * @param mixed  $alternative_value Appropriate value for given type that doesn't match the default value.
     3325     */
     3326    public function test_object_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
     3327        $this->grant_write_permission();
     3328
     3329        $meta_key_multiple = "with_multi_{$type}_default";
     3330
     3331        // Register non-singular post meta for type.
     3332        register_post_meta(
     3333            'post',
     3334            $meta_key_multiple,
     3335            array(
     3336                'type'         => 'object',
     3337                'single'       => false,
     3338                'show_in_rest' => array(
     3339                    'schema' => array(
     3340                        'type'       => 'object',
     3341                        'properties' => array(
     3342                            $type => array( 'type' => $type ),
     3343                        ),
     3344                    ),
     3345                ),
     3346                'default'      => (object) array( $type => $default_value ),
     3347            )
     3348        );
     3349
     3350        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3351        $request->set_body_params(
     3352            array(
     3353                'meta' => array(
     3354                    $meta_key_multiple => array( (object) array( $type => $default_value ) ),
     3355                ),
     3356            )
     3357        );
     3358
     3359        $response = rest_get_server()->dispatch( $request );
     3360        $this->assertSame(
     3361            200,
     3362            $response->get_status(),
     3363            "API call should have returned successfully but didn't: check test setup."
     3364        );
     3365
     3366        // Objects stored into the database are read back as arrays.
     3367        $this->assertSame(
     3368            array( array( $type => $default_value ) ),
     3369            get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
     3370            'Should have stored a single meta value with an object representing the default value.'
     3371        );
     3372
     3373        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3374        $request->set_body_params(
     3375            array(
     3376                'meta' => array(
     3377                    $meta_key_multiple => array(
     3378                        (object) array( $type => $default_value ),
     3379                        (object) array( $type => $alternative_value ),
     3380                    ),
     3381                ),
     3382            )
     3383        );
     3384
     3385        $response = rest_get_server()->dispatch( $request );
     3386        $this->assertSame(
     3387            200,
     3388            $response->get_status(),
     3389            "API call should have returned successfully but didn't: check test setup."
     3390        );
     3391
     3392        // Objects stored into the database are read back as arrays.
     3393        $this->assertSame(
     3394            array( array( $type => $default_value ), array( $type => $alternative_value ) ),
     3395            get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
     3396            'Should have stored a single meta value with an object representing the default value.'
     3397        );
     3398    }
     3399
     3400    /**
     3401     * Ensures that REST API calls with post meta containing a list array as the default
     3402     * value for the registered meta field stores the default value into the database.
     3403     *
     3404     * When the default value isn't persisted in the database, a read of the post meta
     3405     * at some point in the future might return a different value if the code setting the
     3406     * default changed. This ensures that once a value is intentionally saved into the
     3407     * database that it will remain durably in future reads.
     3408     *
     3409     * @ticket 55600
     3410     *
     3411     * @dataProvider data_scalar_default_values
     3412     *
     3413     * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
     3414     * @param mixed  $default_value     Appropriate default value for given type.
     3415     * @param mixed  $alternative_value Ignored in this test.
     3416     */
     3417    public function test_array_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
     3418        $this->grant_write_permission();
     3419
     3420        $meta_key_single = "with_{$type}_default";
     3421
     3422        // Register singular post meta for type.
     3423        register_post_meta(
     3424            'post',
     3425            $meta_key_single,
     3426            array(
     3427                'type'         => 'array',
     3428                'single'       => true,
     3429                'show_in_rest' => array(
     3430                    'schema' => array(
     3431                        'type'  => 'array',
     3432                        'items' => array(
     3433                            'type' => $type,
     3434                        ),
     3435                    ),
     3436                ),
     3437                'default'      => $default_value,
     3438            )
     3439        );
     3440
     3441        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3442        $request->set_body_params(
     3443            array(
     3444                'meta' => array(
     3445                    $meta_key_single => array( $default_value ),
     3446                ),
     3447            )
     3448        );
     3449
     3450        $response = rest_get_server()->dispatch( $request );
     3451        $this->assertSame(
     3452            200,
     3453            $response->get_status(),
     3454            "API call should have returned successfully but didn't: check test setup."
     3455        );
     3456
     3457        $this->assertSame(
     3458            array( array( $default_value ) ),
     3459            get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ),
     3460            'Should have stored a single meta value with an array containing only the default value.'
     3461        );
     3462    }
     3463
     3464    /**
     3465     * Ensures that REST API calls with multi post meta values (containing a list array as
     3466     * the default) for the registered meta field stores the default value into the database.
     3467     *
     3468     * When the default value isn't persisted in the database, a read of the post meta
     3469     * at some point in the future might return a different value if the code setting the
     3470     * default changed. This ensures that once a value is intentionally saved into the
     3471     * database that it will remain durably in future reads.
     3472     *
     3473     * Further, the total count of stored values may be wrong if the default value
     3474     * is culled from the results of a "multi" read.
     3475     *
     3476     * @ticket 55600
     3477     *
     3478     * @dataProvider data_scalar_default_values
     3479     *
     3480     * @param string $type              Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`.
     3481     * @param mixed  $default_value     Appropriate default value for given type.
     3482     * @param mixed  $alternative_value Appropriate value for given type that doesn't match the default value.
     3483     */
     3484    public function test_array_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) {
     3485        $this->grant_write_permission();
     3486
     3487        $meta_key_multiple = "with_multi_{$type}_default";
     3488
     3489        // Register non-singular post meta for type.
     3490        register_post_meta(
     3491            'post',
     3492            $meta_key_multiple,
     3493            array(
     3494                'type'         => 'array',
     3495                'single'       => false,
     3496                'show_in_rest' => array(
     3497                    'schema' => array(
     3498                        'type'  => 'array',
     3499                        'items' => array(
     3500                            'type' => $type,
     3501                        ),
     3502                    ),
     3503                ),
     3504                'default'      => $default_value,
     3505            )
     3506        );
     3507
     3508        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3509        $request->set_body_params(
     3510            array(
     3511                'meta' => array(
     3512                    $meta_key_multiple => array( array( $default_value ) ),
     3513                ),
     3514            )
     3515        );
     3516
     3517        $response = rest_get_server()->dispatch( $request );
     3518        $this->assertSame(
     3519            200,
     3520            $response->get_status(),
     3521            "API call should have returned successfully but didn't: check test setup."
     3522        );
     3523
     3524        $this->assertSame(
     3525            array( array( $default_value ) ),
     3526            get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
     3527            'Should have stored a single meta value with an object representing the default value.'
     3528        );
     3529
     3530        $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     3531        $request->set_body_params(
     3532            array(
     3533                'meta' => array(
     3534                    $meta_key_multiple => array(
     3535                        array( $default_value ),
     3536                        array( $alternative_value ),
     3537                    ),
     3538                ),
     3539            )
     3540        );
     3541
     3542        $response = rest_get_server()->dispatch( $request );
     3543        $this->assertSame(
     3544            200,
     3545            $response->get_status(),
     3546            "API call should have returned successfully but didn't: check test setup."
     3547        );
     3548
     3549        $this->assertSame(
     3550            array( array( $default_value ), array( $alternative_value ) ),
     3551            get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ),
     3552            'Should have stored a single meta value with an object representing the default value.'
     3553        );
     3554    }
     3555
     3556    /**
    30993557     * @ticket 48823
    31003558     */
     
    35173975        );
    35183976    }
     3977
     3978    /**
     3979     * Data provider.
     3980     *
     3981     * Provides example default values of scalar types;
     3982     * in contrast to arrays, objects, etc...
     3983     *
     3984     * @return array[]
     3985     */
     3986    public static function data_scalar_default_values() {
     3987        return array(
     3988            'boolean default' => array( 'boolean', true, false ),
     3989            'integer default' => array( 'integer', 42, 43 ),
     3990            'number default'  => array( 'number', 42.99, 43.99 ),
     3991            'string default'  => array( 'string', 'string', 'string2' ),
     3992        );
     3993    }
    35193994}
Note: See TracChangeset for help on using the changeset viewer.