Make WordPress Core

Changeset 51962


Ignore:
Timestamp:
10/31/2021 11:15:10 PM (3 years ago)
Author:
TimothyBlynJacobs
Message:

REST API: Support custom namespaces for custom post types.

While a custom post type can define a custom route by using the rest_base argument, a namespace of wp/v2 was assumed. This commit introduces support for a rest_namespace argument.

A new rest_get_route_for_post_type_items function has been introduced and the rest_get_route_for_post function updated to facilitate getting the correct route for custom post types.

While the WordPress Core Block Editor bootstrap code has been updated to use these API functions, for maximum compatibility sticking with the default wp/v2 namespace is recommended until the API functions see wider use.

Props spacedmonkey, swissspidy.
Fixes #53656.

Location:
trunk
Files:
16 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/edit-form-blocks.php

    r51657 r51962  
    5050wp_enqueue_script( 'wp-edit-post' );
    5151
    52 $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
     52$rest_path = rest_get_route_for_post( $post );
    5353
    5454// Preload common data.
     
    5858    '/wp/v2/taxonomies?per_page=-1&context=edit',
    5959    '/wp/v2/themes?status=active',
    60     sprintf( '/wp/v2/%s/%s?context=edit', $rest_base, $post->ID ),
     60    add_query_arg( 'context', 'edit', $rest_path ),
    6161    sprintf( '/wp/v2/types/%s?context=edit', $post_type ),
    6262    sprintf( '/wp/v2/users/me?post_type=%s&context=edit', $post_type ),
    63     array( '/wp/v2/media', 'OPTIONS' ),
    64     array( '/wp/v2/blocks', 'OPTIONS' ),
    65     sprintf( '/wp/v2/%s/%d/autosaves?context=edit', $rest_base, $post->ID ),
     63    array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ),
     64    array( rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ),
     65    sprintf( '%s/autosaves?context=edit', $rest_path ),
    6666);
    6767
  • trunk/src/wp-admin/widgets-form-blocks.php

    r51700 r51962  
    1919
    2020$preload_paths = array(
    21     array( '/wp/v2/media', 'OPTIONS' ),
     21    array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ),
    2222    '/wp/v2/sidebars?context=edit&per_page=-1',
    2323    '/wp/v2/widgets?context=edit&per_page=-1&_embed=about',
  • trunk/src/wp-includes/class-wp-post-type.php

    r49790 r51962  
    358358     */
    359359    public $rest_base;
     360
     361    /**
     362     * The namespace for this post type's REST API endpoints.
     363     *
     364     * @since 5.9
     365     * @var string|bool $rest_namespace
     366     */
     367    public $rest_namespace;
    360368
    361369    /**
     
    453461            'show_in_rest'          => false,
    454462            'rest_base'             => false,
     463            'rest_namespace'        => false,
    455464            'rest_controller_class' => false,
    456465            'template'              => array(),
     
    472481        if ( null === $args['show_ui'] ) {
    473482            $args['show_ui'] = $args['public'];
     483        }
     484
     485        // If not set, default rest_namespace to wp/v2 if show_in_rest is true.
     486        if ( false === $args['rest_namespace'] && ! empty( $args['show_in_rest'] ) ) {
     487            $args['rest_namespace'] = 'wp/v2';
    474488        }
    475489
  • trunk/src/wp-includes/post.php

    r51913 r51962  
    14191419 *                                               for the post type to be available in the block editor.
    14201420 *     @type string       $rest_base             To change the base URL of REST API route. Default is $post_type.
     1421 *     @type string       $rest_namespace        To change the namespace URL of REST API route. Default is wp/v2.
    14211422 *     @type string       $rest_controller_class REST API controller class name. Default is 'WP_REST_Posts_Controller'.
    14221423 *     @type int          $menu_position         The position in the menu order the post type should appear. To work,
  • trunk/src/wp-includes/rest-api.php

    r51958 r51962  
    30503050    }
    30513051
    3052     $post_type = get_post_type_object( $post->post_type );
    3053     if ( ! $post_type ) {
     3052    $post_type_route = rest_get_route_for_post_type_items( $post->post_type );
     3053    if ( ! $post_type_route ) {
    30543054        return '';
    30553055    }
    30563056
    3057     $controller = $post_type->get_rest_controller();
    3058     if ( ! $controller ) {
    3059         return '';
    3060     }
    3061 
    3062     $route = '';
    3063 
    3064     // The only two controllers that we can detect are the Attachments and Posts controllers.
    3065     if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) {
    3066         $namespace = 'wp/v2';
    3067         $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
    3068         $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID );
    3069     }
     3057    $route = sprintf( '%s/%d', $post_type_route, $post->ID );
    30703058
    30713059    /**
     
    30813069
    30823070/**
     3071 * Gets the REST API route for a post type.
     3072 *
     3073 * @since 5.9.0
     3074 *
     3075 * @param string $post_type The name of a registered post type.
     3076 * @return string The route path with a leading slash for the given post type, or an empty string if there is not a route.
     3077 */
     3078function rest_get_route_for_post_type_items( $post_type ) {
     3079    $post_type = get_post_type_object( $post_type );
     3080    if ( ! $post_type ) {
     3081        return '';
     3082    }
     3083
     3084    if ( ! $post_type->show_in_rest ) {
     3085        return '';
     3086    }
     3087
     3088    $namespace = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2';
     3089    $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
     3090    $route     = sprintf( '/%s/%s', $namespace, $rest_base );
     3091
     3092    /**
     3093     * Filters the REST API route for a post type.
     3094     *
     3095     * @since 5.9.0
     3096     *
     3097     * @param string       $route      The route path.
     3098     * @param WP_Post_Type $post_type  The post type object.
     3099     */
     3100    return apply_filters( 'rest_route_for_post_type_items', $route, $post_type );
     3101}
     3102
     3103/**
    30833104 * Gets the REST API route for a term.
    30843105 *
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r51960 r51962  
    12891289            $response->add_link(
    12901290                'https://api.w.org/featuredmedia',
    1291                 rest_url( 'wp/v2/media/' . $site_logo_id ),
     1291                rest_url( rest_get_route_for_post( $site_logo_id ) ),
    12921292                array(
    12931293                    'embeddable' => true,
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php

    r51786 r51962  
    6868        $this->parent_controller    = $parent_controller;
    6969        $this->revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
    70         $this->namespace            = 'wp/v2';
    7170        $this->rest_base            = 'autosaves';
     71        $this->namespace            = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
    7272        $this->parent_base          = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
    7373    }
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-statuses-controller.php

    r51786 r51962  
    264264        $response = rest_ensure_response( $data );
    265265
     266        $rest_url = rest_url( rest_get_route_for_post_type_items( 'post' ) );
    266267        if ( 'publish' === $status->name ) {
    267             $response->add_link( 'archives', rest_url( 'wp/v2/posts' ) );
     268            $response->add_link( 'archives', $rest_url );
    268269        } else {
    269             $response->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( 'wp/v2/posts' ) ) );
     270            $response->add_link( 'archives', add_query_arg( 'status', $status->name, $rest_url ) );
    270271        }
    271272
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php

    r51961 r51962  
    181181        $taxonomies = wp_list_pluck( $taxonomies, 'name' );
    182182        $base       = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
     183        $namespace  = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2';
    183184        $supports   = get_all_post_type_supports( $post_type->name );
    184185
     
    233234        }
    234235
     236        if ( in_array( 'rest_namespace', $fields, true ) ) {
     237            $data['rest_namespace'] = $namespace;
     238        }
     239
    235240        $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
    236241        $data    = $this->add_additional_fields_to_object( $data, $request );
     
    246251                ),
    247252                'https://api.w.org/items' => array(
    248                     'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ),
     253                    'href' => rest_url( rest_get_route_for_post_type_items( $post_type->name ) ),
    249254                ),
    250255            )
     
    270275     * @since 4.7.0
    271276     * @since 4.8.0 The `supports` property was added.
    272      * @since 5.9.0 The `visibility` property was added.
     277     * @since 5.9.0 The `visibility` and `rest_namespace` properties were added.
    273278     *
    274279     * @return array Item schema data.
     
    284289            'type'       => 'object',
    285290            'properties' => array(
    286                 'capabilities' => array(
     291                'capabilities'   => array(
    287292                    'description' => __( 'All capabilities used by the post type.' ),
    288293                    'type'        => 'object',
     
    290295                    'readonly'    => true,
    291296                ),
    292                 'description'  => array(
     297                'description'    => array(
    293298                    'description' => __( 'A human-readable description of the post type.' ),
    294299                    'type'        => 'string',
     
    296301                    'readonly'    => true,
    297302                ),
    298                 'hierarchical' => array(
     303                'hierarchical'   => array(
    299304                    'description' => __( 'Whether or not the post type should have children.' ),
    300305                    'type'        => 'boolean',
     
    302307                    'readonly'    => true,
    303308                ),
    304                 'viewable'     => array(
     309                'viewable'       => array(
    305310                    'description' => __( 'Whether or not the post type can be viewed.' ),
    306311                    'type'        => 'boolean',
     
    308313                    'readonly'    => true,
    309314                ),
    310                 'labels'       => array(
     315                'labels'         => array(
    311316                    'description' => __( 'Human-readable labels for the post type for various contexts.' ),
    312317                    'type'        => 'object',
     
    314319                    'readonly'    => true,
    315320                ),
    316                 'name'         => array(
     321                'name'           => array(
    317322                    'description' => __( 'The title for the post type.' ),
    318323                    'type'        => 'string',
     
    320325                    'readonly'    => true,
    321326                ),
    322                 'slug'         => array(
     327                'slug'           => array(
    323328                    'description' => __( 'An alphanumeric identifier for the post type.' ),
    324329                    'type'        => 'string',
     
    326331                    'readonly'    => true,
    327332                ),
    328                 'supports'     => array(
     333                'supports'       => array(
    329334                    'description' => __( 'All features, supported by the post type.' ),
    330335                    'type'        => 'object',
     
    332337                    'readonly'    => true,
    333338                ),
    334                 'taxonomies'   => array(
     339                'taxonomies'     => array(
    335340                    'description' => __( 'Taxonomies associated with post type.' ),
    336341                    'type'        => 'array',
     
    341346                    'readonly'    => true,
    342347                ),
    343                 'rest_base'    => array(
     348                'rest_base'      => array(
    344349                    'description' => __( 'REST base route for the post type.' ),
    345350                    'type'        => 'string',
     
    347352                    'readonly'    => true,
    348353                ),
    349                 'visibility'   => array(
     354                'rest_namespace' => array(
     355                    'description' => __( 'REST route\'s namespace for the post type.' ),
     356                    'type'        => 'string',
     357                    'context'     => array( 'view', 'edit', 'embed' ),
     358                    'readonly'    => true,
     359                ),
     360                'visibility'     => array(
    350361                    'description' => __( 'The visibility settings for the post type.' ),
    351362                    'type'        => 'object',
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

    r51786 r51962  
    4949    public function __construct( $post_type ) {
    5050        $this->post_type = $post_type;
    51         $this->namespace = 'wp/v2';
    5251        $obj             = get_post_type_object( $post_type );
    5352        $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
     53        $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
    5454
    5555        $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
     
    20382038        $featured_media = get_post_thumbnail_id( $post->ID );
    20392039        if ( $featured_media ) {
    2040             $image_url = rest_url( 'wp/v2/media/' . $featured_media );
     2040            $image_url = rest_url( rest_get_route_for_post( $featured_media ) );
    20412041
    20422042            $links['https://api.w.org/featuredmedia'] = array(
     
    20472047
    20482048        if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
    2049             $attachments_url = rest_url( 'wp/v2/media' );
     2049            $attachments_url = rest_url( rest_get_route_for_post_type_items( 'attachment' ) );
    20502050            $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
    20512051
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php

    r51786 r51962  
    5050    public function __construct( $parent_post_type ) {
    5151        $this->parent_post_type  = $parent_post_type;
    52         $this->namespace         = 'wp/v2';
    5352        $this->rest_base         = 'revisions';
    5453        $post_type_object        = get_post_type_object( $parent_post_type );
    5554        $this->parent_base       = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
     55        $this->namespace         = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
    5656        $this->parent_controller = $post_type_object->get_rest_controller();
    5757
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php

    r51786 r51962  
    3434    public function __construct( $post_type ) {
    3535        $this->post_type = $post_type;
    36         $this->namespace = 'wp/v2';
    3736        $obj             = get_post_type_object( $post_type );
    3837        $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
     38        $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
    3939    }
    4040
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php

    r51293 r51962  
    915915
    916916        foreach ( $taxonomy_obj->object_type as $type ) {
    917             $post_type_object = get_post_type_object( $type );
    918 
    919             if ( empty( $post_type_object->show_in_rest ) ) {
     917            $rest_path = rest_get_route_for_post_type_items( $type );
     918
     919            if ( empty( $rest_path ) ) {
    920920                continue;
    921921            }
    922922
    923             $rest_base         = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
    924923            $post_type_links[] = array(
    925                 'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( sprintf( 'wp/v2/%s', $rest_base ) ) ),
     924                'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( $rest_path ) ),
    926925            );
    927926        }
  • trunk/tests/phpunit/tests/rest-api.php

    r51648 r51962  
    18311831
    18321832    /**
     1833     * @ticket 53656
     1834     */
     1835    public function test_rest_get_route_for_post_custom_namespace() {
     1836        register_post_type(
     1837            'cpt',
     1838            array(
     1839                'show_in_rest'   => true,
     1840                'rest_base'      => 'cpt',
     1841                'rest_namespace' => 'wordpress/v1',
     1842            )
     1843        );
     1844        $post = self::factory()->post->create_and_get( array( 'post_type' => 'cpt' ) );
     1845
     1846        $this->assertSame( '/wordpress/v1/cpt/' . $post->ID, rest_get_route_for_post( $post ) );
     1847        unregister_post_type( 'cpt' );
     1848    }
     1849
     1850    /**
     1851     * @ticket 53656
     1852     */
     1853    public function test_rest_get_route_for_post_type_items() {
     1854        $this->assertSame( '/wp/v2/posts', rest_get_route_for_post_type_items( 'post' ) );
     1855    }
     1856
     1857    /**
     1858     * @ticket 53656
     1859     */
     1860    public function test_rest_get_route_for_post_type_items_custom_namespace() {
     1861        register_post_type(
     1862            'cpt',
     1863            array(
     1864                'show_in_rest'   => true,
     1865                'rest_base'      => 'cpt',
     1866                'rest_namespace' => 'wordpress/v1',
     1867            )
     1868        );
     1869
     1870        $this->assertSame( '/wordpress/v1/cpt', rest_get_route_for_post_type_items( 'cpt' ) );
     1871        unregister_post_type( 'cpt' );
     1872    }
     1873
     1874    /**
    18331875     * @ticket 49116
    18341876     */
     
    18401882    /**
    18411883     * @ticket 49116
     1884     * @ticket 53656
    18421885     */
    18431886    public function test_rest_get_route_for_post_custom_controller() {
    18441887        $post = self::factory()->post->create_and_get( array( 'post_type' => 'wp_block' ) );
    1845         $this->assertSame( '', rest_get_route_for_post( $post ) );
     1888        $this->assertSame( '/wp/v2/blocks/' . $post->ID, rest_get_route_for_post( $post ) );
    18461889    }
    18471890
  • trunk/tests/phpunit/tests/rest-api/rest-post-types-controller.php

    r51959 r51962  
    6363    }
    6464
     65    /**
     66     * @ticket 53656
     67     */
     68    public function test_get_item_cpt() {
     69        register_post_type(
     70            'cpt',
     71            array(
     72                'show_in_rest'   => true,
     73                'rest_base'      => 'cpt',
     74                'rest_namespace' => 'wordpress/v1',
     75            )
     76        );
     77        $request  = new WP_REST_Request( 'GET', '/wp/v2/types/cpt' );
     78        $response = rest_get_server()->dispatch( $request );
     79        $this->check_post_type_object_response( 'view', $response, 'cpt' );
     80    }
     81
    6582    public function test_get_item_page() {
    6683        $request  = new WP_REST_Request( 'GET', '/wp/v2/types/page' );
     
    145162        $data       = $response->get_data();
    146163        $properties = $data['schema']['properties'];
    147         $this->assertCount( 11, $properties );
     164        $this->assertCount( 12, $properties );
    148165        $this->assertArrayHasKey( 'capabilities', $properties );
    149166        $this->assertArrayHasKey( 'description', $properties );
     
    156173        $this->assertArrayHasKey( 'taxonomies', $properties );
    157174        $this->assertArrayHasKey( 'rest_base', $properties );
     175        $this->assertArrayHasKey( 'rest_namespace', $properties );
    158176        $this->assertArrayHasKey( 'visibility', $properties );
    159177    }
     
    205223        $this->assertSame( $post_type_obj->hierarchical, $data['hierarchical'] );
    206224        $this->assertSame( $post_type_obj->rest_base, $data['rest_base'] );
     225        $this->assertSame( $post_type_obj->rest_namespace, $data['rest_namespace'] );
    207226
    208227        $links = test_rest_expand_compact_links( $links );
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r51943 r51962  
    83378337        ],
    83388338        "rest_base": "posts",
     8339        "rest_namespace": "wp/v2",
    83398340        "_links": {
    83408341            "collection": [
     
    83648365        "taxonomies": [],
    83658366        "rest_base": "pages",
     8367        "rest_namespace": "wp/v2",
    83668368        "_links": {
    83678369            "collection": [
     
    83918393        "taxonomies": [],
    83928394        "rest_base": "media",
     8395        "rest_namespace": "wp/v2",
    83938396        "_links": {
    83948397            "collection": [
     
    84188421        "taxonomies": [],
    84198422        "rest_base": "blocks",
     8423        "rest_namespace": "wp/v2",
    84208424        "_links": {
    84218425            "collection": [
     
    84458449        "taxonomies": [],
    84468450        "rest_base": "templates",
     8451        "rest_namespace": "wp/v2",
    84478452        "_links": {
    84488453            "collection": [
     
    84768481        "post_tag"
    84778482    ],
    8478     "rest_base": "posts"
     8483    "rest_base": "posts",
     8484    "rest_namespace": "wp/v2"
    84798485};
    84808486
Note: See TracChangeset for help on using the changeset viewer.