Experiment: Taxonomies REST controller#77697
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: -57 B (0%) Total Size: 7.82 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in 16e5f2d. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25039567310
|
7f2a62a to
f479a6b
Compare
d4fecc6 to
16eaabc
Compare
16eaabc to
389df11
Compare
| foreach ( gutenberg_user_taxonomy_allowed_label_keys() as $label_key ) { | ||
| $label_schema['properties'][ $label_key ] = array( | ||
| 'type' => 'string', | ||
| 'maxLength' => 200, |
| 'hierarchical' => array( 'type' => 'boolean' ), | ||
| 'description' => array( | ||
| 'type' => 'string', | ||
| 'maxLength' => 1000, |
| // Empty config must serialize as `{}` to match the schema's | ||
| // `type: 'object'`. PHP encodes empty arrays as `[]`, so cast | ||
| // to stdClass for the empty case. | ||
| $data['config'] = empty( $config ) ? new stdClass() : $config; |
There was a problem hiding this comment.
This makes sense but feels a bit messy. Are we doing it anywhere else?
| public function get_collection_params() { | ||
| $params = parent::get_collection_params(); | ||
|
|
||
| $params['object_type'] = array( |
There was a problem hiding this comment.
I imagine that's a future feature to use in the post types UI?
| * @return string[] | ||
| */ | ||
| function gutenberg_user_taxonomy_read_object_type( $post_id ) { | ||
| $values = get_post_meta( $post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY ); |
There was a problem hiding this comment.
Are we fully dropping the object_type that was inside config before?
| $prepared->post_content = wp_json_encode( | ||
| $config, | ||
| JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | ||
| ); |
There was a problem hiding this comment.
In another place we will convert empty configs to objects, but not here. Why?
| $response = parent::prepare_item_for_response( $item, $request ); | ||
| $data = $response->get_data(); | ||
|
|
||
| unset( $data['content'] ); |
| * Allowed keys inside the stored taxonomy config. Anything outside this list | ||
| * is dropped by the sanitizer — this is the structural allowlist that backs | ||
| * `additionalProperties: false` in the REST schema. |
There was a problem hiding this comment.
Should we also mention that this should be in sync with STRING_LABEL_KEYS? Is there a better way to ensure we keep them in sync?
Part of: #77600
What
Refactors the
wp_user_taxonomyREST surface and its write-time sanitization model. Stops exposing the rawpost_contentJSON string over REST and replaces it with a typedconfigobject plus a top-levelobject_typearray. Sanitization of the stored payload moves to a single site — a post-type-scoped filter onwp_insert_post_data— instead of being scattered across the controller, the registration step, and the schema layer. Adds filterable post-type membership via?object_type[]=and wires the correspondingisAnyfilter into the listing UI.Builds on
taxonomies/add-edit-more-fields-and-separate-pages(approved, merging shortly). Once that lands, the base flips totrunk.How
The previous shape stored JSON in
post_contentand forced both client and consumers to parse strings. That made the typed schema (label allowlist,additionalProperties: false,maxLengths) impossible to enforce at the REST layer, made_fields=config.labels.singular_namequeries useless, and required the client to JSON-stringify on every save. It also relied on per-call sanitization in three places, which drifts.The new shape pushes structure into the schema (so REST validates inputs, and clients can introspect/filter on inner fields), and concentrates write-time sanitization in one site. The sanitizer is hooked to
wp_insert_post_data, scoped bypost_type === 'wp_user_taxonomy'(plus revisions whose parent is awp_user_taxonomypost). Stored JSON is encoded withJSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP, so the bytes contain no live<,>, or&characters — kses sees an inert envelope, ordering vswp_filter_post_ksesdoesn't matter, and there's nothing to mangle on the round-trip.The filter is unconditional — taxonomy config isn't HTML and shouldn't carry scripts even for users with
unfiltered_html. Imports, XML-RPC, REST writes, and revision restores all pass throughwp_insert_post()and therefore through the filter, so no separateimport_start/import_endhandling is needed.(An earlier revision of this PR mirrored core's
wp_filter_global_styles_postshape —content_save_preat priority 9, with anisUserTaxonomyConfigJSONmarker to self-identify on a hook that lacks post-type context. Per @jorgefilipecosta's review, the post-type-scoped form is the cleaner fit here: no marker is needed because$data['post_type']already scopes the filter, the priority-9 ordering isn't load-bearing once the encoder uses HEX flags, and the work runs only for our CPT instead of every site-wide post save.)Server changes
WP_REST_User_Taxonomies_Controller_Gutenberg. Hides rawcontent, adds a strictconfigobject schema (label allowlist with per-keymaxLength,additionalProperties: falseeverywhere, post-type slug pattern^[a-z0-9_-]{1,20}$matching thewp_posts.post_typecolumn width). Adds anobject_type[]collection param backed bymeta_query IN.prepare_item_for_databaseencodes the requestconfigas-is; thewp_insert_post_datafilter does the structural sanitize before the row is written.gutenberg_filter_user_taxonomy_post_contentonwp_insert_post_data. Acts onwp_user_taxonomyposts and revisions of taxonomy posts; returns input unchanged for any other post type or invalid JSON. Re-encodes withJSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP.object_typemoved to multi-value post meta (_wp_user_taxonomy_object_type). Stored separately from the JSON config so listings can filter on it viameta_query(a single serialized array would force fragileLIKEqueries). Underscore-prefixed for protected-meta semantics. Surfaced over REST as the typed top-levelobject_typefield.gutenberg_user_taxonomy_sanitize_config— only called from thewp_insert_post_datafilter; controller reads/writes trust storage.Client changes
TaxonomyRecordandTaxonomyFormDataare now typed (config: StoredConfig,object_type: string[]). ThetoFormData/serializeForSavehelpers become thin translators between wire and form shape — no more JSON parse/stringify on every save.useObjectTypeFieldenables the post-types column filter (filterBy: { operators: [ 'isAny' ] }).routes/taxonomies/stage.tsxtranslates theobject_typeview filter into the REST query arg.Notes
Taxonomies created before this PR aren't migrated. Their attached post types lived inside the JSON config in
post_content; the new code reads them from the_wp_user_taxonomy_object_typepost meta, which those older records don't have. To get them showing post types again in the UI, delete and recreate them. (The feature is still experimental and pre-launch, so no production data is at risk.)Testing instructions
post typesfilterUse of AI Tools
Opus 4.7 with direction, changes and review