Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ public static function normalize_config_for_encode( $value, $schema = null ) {
/**
* Validates the prepared post's `post_name` slug. Rejects:
* - shapes outside `^[a-z0-9_-]{1,20}$`,
* - slugs that collide with a WP query var (`cat`, `author`, …),
* - slugs already taken by another `wp_user_post_type` post,
* - slugs reserved by an existing registered post type (core/plugin).
*
Expand Down Expand Up @@ -471,6 +472,22 @@ private function validate_slug( $prepared ) {
}
}

// Reject WP query-var collisions (`cat`, `author`, `name`, …). These
// names aren't registered as post types, so `post_type_exists()` below
// won't catch them, but registering a post type with one breaks
// built-in archive URLs via rewrite collisions.
if ( $this->is_reserved_query_var( $slug ) ) {
return new WP_Error(
'gutenberg_user_post_type_slug_query_var_reserved',
sprintf(
/* translators: %s: post type slug */
__( 'The post type key "%s" is reserved by WordPress and would conflict with built-in URLs.', 'gutenberg' ),
$slug
),
array( 'status' => 400 )
);
}

$other_posts = get_posts(
array(
'post_type' => 'wp_user_post_type',
Expand Down Expand Up @@ -508,6 +525,22 @@ private function validate_slug( $prepared ) {
return true;
}

/**
* Whether the given slug collides with a WP public or private query var.
* Registering a taxonomy or post type with one of these names creates
* rewrite rules that hijack built-in archive URLs (e.g. `?cat=`,
* `?author=`). Uses `$wp->public_query_vars` as the canonical source so
* the list stays in sync with whatever core declares at runtime.
*
* @param string $slug Candidate slug.
* @return bool
*/
private function is_reserved_query_var( $slug ) {
global $wp;
return in_array( $slug, $wp->public_query_vars, true )
|| in_array( $slug, $wp->private_query_vars, true );
}

/**
* Reapplies the user-taxonomy half of a `config.taxonomies` write. Walks
* the requested slugs once, finds the user-defined ones, and reconciles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ public static function normalize_config_for_encode( $value, $schema = null ) {
/**
* Validates the prepared post's `post_name` slug. Rejects:
* - shapes outside `^[a-z0-9_-]{1,32}$`,
* - slugs that collide with a WP query var (`cat`, `author`, …),
* - slugs already taken by another `wp_user_taxonomy` post,
* - slugs reserved by an existing registered taxonomy (core/plugin).
*
Expand Down Expand Up @@ -287,6 +288,22 @@ private function validate_slug( $prepared ) {
}
}

// Reject WP query-var collisions (`cat`, `author`, `name`, …). These
// names aren't registered as taxonomies, so `taxonomy_exists()` below
// won't catch them, but registering a taxonomy with one breaks
// built-in archive URLs via rewrite collisions.
if ( $this->is_reserved_query_var( $slug ) ) {
return new WP_Error(
'gutenberg_user_taxonomy_slug_query_var_reserved',
sprintf(
/* translators: %s: taxonomy slug */
__( 'The taxonomy key "%s" is reserved by WordPress and would conflict with built-in URLs.', 'gutenberg' ),
$slug
),
array( 'status' => 400 )
);
}

$other_posts = get_posts(
array(
'post_type' => 'wp_user_taxonomy',
Expand Down Expand Up @@ -324,6 +341,22 @@ private function validate_slug( $prepared ) {
return true;
}

/**
* Whether the given slug collides with a WP public or private query var.
* Registering a taxonomy or post type with one of these names creates
* rewrite rules that hijack built-in archive URLs (e.g. `?cat=`,
* `?author=`). Uses `$wp->public_query_vars` as the canonical source so
* the list stays in sync with whatever core declares at runtime.
*
* @param string $slug Candidate slug.
* @return bool
*/
private function is_reserved_query_var( $slug ) {
global $wp;
return in_array( $slug, $wp->public_query_vars, true )
|| in_array( $slug, $wp->private_query_vars, true );
}

/**
* Creates a single record, then writes the `object_type` post meta from
* the request and re-prepares the response so the just-written meta is
Expand Down
119 changes: 119 additions & 0 deletions packages/content-types/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,122 @@ export const TAXONOMY_ENTITY = 'wp_user_taxonomy';

// Sentinel for the edit-route param when creating a new record.
export const NEW_ID = 'new';

// Reserved post type slugs per `register_post_type()` docs.
// @see https://developer.wordpress.org/reference/functions/register_post_type/#reserved-post-types.
// The PHP REST controller is authoritative (it also consults
// `$wp->public_query_vars` for rewrite-collision safety); this list drives
// inline form validation.
export const RESERVED_POST_TYPE_KEYS = new Set( [
'post',
'page',
'attachment',
'revision',
'nav_menu_item',
'custom_css',
'customize_changeset',
'oembed_cache',
'user_request',
'wp_block',
'wp_global_styles',
'wp_navigation',
'wp_template',
'wp_template_part',
'action',
'author',
'order',
'theme',
] );

// Reserved taxonomy slugs per `register_taxonomy()` docs.
// @see https://developer.wordpress.org/reference/functions/register_taxonomy/#reserved-terms.
// The PHP REST controller is authoritative (it also consults
// `$wp->public_query_vars` for rewrite-collision safety); this list drives
// inline form validation.
export const RESERVED_TAXONOMY_KEYS = new Set( [
'attachment',
'attachment_id',
'author',
'author_name',
'calendar',
'cat',
'category',
'category__and',
'category__in',
'category__not_in',
'category_name',
'comments_per_page',
'comments_popup',
'custom',
'customize_messenger_channel',
'customized',
'cpage',
'day',
'debug',
'embed',
'error',
'exact',
'feed',
'fields',
'hour',
'link_category',
'm',
'minute',
'monthnum',
'more',
'name',
'nav_menu',
'nonce',
'nopaging',
'offset',
'order',
'orderby',
'p',
'page',
'page_id',
'paged',
'pagename',
'pb',
'perm',
'post',
'post__in',
'post__not_in',
'post_format',
'post_mime_type',
'post_status',
'post_tag',
'post_type',
'posts',
'posts_per_archive_page',
'posts_per_page',
'preview',
'robots',
's',
'search',
'second',
'sentence',
'showposts',
'static',
'status',
'subpost',
'subpost_id',
'tag',
'tag__and',
'tag__in',
'tag__not_in',
'tag_id',
'tag_slug__and',
'tag_slug__in',
'taxonomy',
'tb',
'term',
'terms',
'theme',
'title',
'type',
'types',
'w',
'withcomments',
'withoutcomments',
'year',
] );
7 changes: 6 additions & 1 deletion packages/content-types/src/post-types/fields/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Notice, Stack } from '@wordpress/ui';
/**
* Internal dependencies
*/
import { POST_TYPE_ENTITY } from '../../constants';
import { POST_TYPE_ENTITY, RESERVED_POST_TYPE_KEYS } from '../../constants';
import {
createBooleanField,
createDescriptionField,
Expand Down Expand Up @@ -161,6 +161,11 @@ export function useSlugField(
if ( originalSlug !== undefined && slug === originalSlug ) {
return null;
}
if ( RESERVED_POST_TYPE_KEYS.has( slug ) ) {
return __(
'This key is reserved by WordPress and would conflict with built-in URLs.'
);
}
const slugTaken = ( registeredPostTypes ?? [] ).some(
( pt: any ) => pt.slug === slug
);
Expand Down
7 changes: 6 additions & 1 deletion packages/content-types/src/taxonomies/fields/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Notice, Stack } from '@wordpress/ui';
/**
* Internal dependencies
*/
import { TAXONOMY_ENTITY } from '../../constants';
import { RESERVED_TAXONOMY_KEYS, TAXONOMY_ENTITY } from '../../constants';
import {
createBooleanField,
createDescriptionField,
Expand Down Expand Up @@ -82,6 +82,11 @@ export function useSlugField(
if ( originalSlug !== undefined && slug === originalSlug ) {
return null;
}
if ( RESERVED_TAXONOMY_KEYS.has( slug ) ) {
return __(
'This key is reserved by WordPress and would conflict with built-in URLs.'
);
}
const slugTaken = ( registeredTaxonomies ?? [] ).some(
( t: any ) => t.slug === slug
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,33 @@ public function test_create_item_rejects_duplicate_slug() {
$this->assertSame( 'gutenberg_user_post_type_slug_taken', $response->get_data()['code'] );
}

/**
* Slugs that collide with a WP query var (e.g. `cat`, `author`, `name`)
* are rejected with the dedicated query-var-collision error code. These
* names aren't themselves registered post types, so the existing
* `post_type_exists()` branch wouldn't catch them — registering a
* post type with one would silently hijack built-in archive URLs.
*/
public function test_create_item_rejects_query_var_slug() {
wp_set_current_user( self::$admin_id );

$request = new WP_REST_Request( 'POST', self::REST_BASE );
$request->set_body_params(
array(
'slug' => 'author',
'title' => 'Author',
'config' => array( 'labels' => array( 'singular_name' => 'Author' ) ),
)
);

$response = rest_get_server()->dispatch( $request );
$this->assertSame( 400, $response->get_status() );
$this->assertSame(
'gutenberg_user_post_type_slug_query_var_reserved',
$response->get_data()['code']
);
}

/**
* Slug must match `^[a-z0-9_-]{1,20}$`. The 20-char cap is post-types-
* specific (vs 32 for taxonomies) because `wp_posts.post_type` is a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,33 @@ public function test_create_item_rejects_duplicate_slug() {
$this->assertSame( 'gutenberg_user_taxonomy_slug_taken', $response->get_data()['code'] );
}

/**
* Slugs that collide with a WP query var (e.g. `cat`, `author`, `name`)
* are rejected with the dedicated query-var-collision error code. These
* names aren't themselves registered taxonomies, so the existing
* `taxonomy_exists()` branch wouldn't catch them — registering a
* taxonomy with one would silently hijack built-in archive URLs.
*/
public function test_create_item_rejects_query_var_slug() {
wp_set_current_user( self::$admin_id );

$request = new WP_REST_Request( 'POST', self::REST_BASE );
$request->set_body_params(
array(
'slug' => 'cat',
'title' => 'Cat',
'config' => array( 'labels' => array( 'singular_name' => 'Cat' ) ),
)
);

$response = rest_get_server()->dispatch( $request );
$this->assertSame( 400, $response->get_status() );
$this->assertSame(
'gutenberg_user_taxonomy_slug_query_var_reserved',
$response->get_data()['code']
);
}

/**
* Update modifies the typed `config` and reflects it back.
*/
Expand Down
Loading