PluginProbe
Gutenberg / 23.3.0
Gutenberg v23.3.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / experimental / content-types / index.php

index.php in Gutenberg 23.3.0, at lib/experimental/content-types/index.php

468 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Registers the private CPTs that store user-defined content types:
4 * - wp_user_taxonomy (user-defined taxonomies)
5 * - wp_user_post_type (user-defined post types)
6 *
7 * Each record holds the registration intent for one taxonomy or post type.
8 * On `init`, the corresponding files read each published record and call
9 * `register_taxonomy()` / `register_post_type()` for it.
10 *
11 * @package gutenberg
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 require_once __DIR__ . '/class-wp-rest-user-taxonomies-controller-gutenberg.php';
19 require_once __DIR__ . '/class-wp-rest-user-post-types-controller-gutenberg.php';
20
21 /**
22 * Post meta key that stores the post types attached to a user-defined
23 * taxonomy. Stored as meta (rather than inside the `post_content` JSON
24 * with the rest of the config) so listings can filter on it via
25 * `meta_query`. Underscore-prefixed so it's treated as protected meta.
26 * Surfaced in REST as the typed top-level `object_type` field.
27 */
28 const GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY = '_wp_user_taxonomy_object_type';
29
30 /**
31 * Self-identifying key embedded in stored `post_content` JSON. Mirrors
32 * core's `isGlobalStylesUserThemeJSON` for `wp_global_styles`.
33 *
34 * Not load-bearing today: writes are sanitized via `wp_insert_post_data`,
35 * which carries `post_type` context, so payload identification doesn't
36 * need a marker. The marker is preserved as a forward-compat anchor for
37 * a content-only fallback sanitizer — e.g., if a future write path turns
38 * out to bypass `wp_insert_post_data`, or a kses-ordering issue forces a
39 * fallback to `content_save_pre`. In those scenarios a content-only
40 * sanitizer can't safely identify our payloads without a marker, and
41 * retroactively migrating stored records across WP installs is not
42 * practical, so the marker is present from day one.
43 *
44 * Storage-only: kept out of the REST schema and stripped on read so it
45 * never reaches clients.
46 */
47 const GUTENBERG_USER_TAXONOMY_CONFIG_MARKER = 'isUserTaxonomyConfigJSON';
48
49 /**
50 * Regex for a valid taxonomy slug. 32 chars matches the `wp_terms.slug`
51 * column width.
52 */
53 const GUTENBERG_USER_TAXONOMY_SLUG_PATTERN = '/^[a-z0-9_-]{1,32}$/';
54
55 /**
56 * Option key flagging that the next `init` should regenerate rewrite rules.
57 * Set by the REST controllers when a wp_user_post_type or wp_user_taxonomy
58 * write changes a field that affects rewrite rules (slug, has_archive,
59 * public) or toggles whether the record is registered.
60 */
61 const GUTENBERG_USER_CONTENT_TYPES_FLUSH_OPTION = '_gutenberg_user_content_types_flush_rewrite_rules';
62
63 /**
64 * Flags the next `init` to regenerate rewrite rules. Flushing inline in the
65 * REST handler would regenerate against the pre-update registration — the
66 * post type / taxonomy registers at `init` priority 20, before the REST
67 * request handler runs, so by the time we'd flush, the registered state
68 * still reflects the old record. Deferring to the next request's
69 * `init` (priority 30, after registration runs against the new record)
70 * picks up the new rewrite rules.
71 */
72 function gutenberg_user_content_types_schedule_flush_rewrite_rules() {
73 // Autoload off — the option is short-lived (cleared on next init), and
74 // keeping it out of the alloptions cache means a stray uncleared row
75 // can't bloat every page load.
76 update_option( GUTENBERG_USER_CONTENT_TYPES_FLUSH_OPTION, '1', false );
77 }
78
79 /**
80 * Regenerates the rewrite rules option if a recent write scheduled a flush.
81 *
82 * Runs at `init` priority 30 — after `register_post_type()` and
83 * `register_taxonomy()` have been called for user-defined records at
84 * priority 20, so the regenerated rules see the new registration state.
85 *
86 * Soft flush only — the standard `.htaccess` block doesn't depend on
87 * individual post type or taxonomy registrations, so there's nothing for
88 * a hard flush to update there, and avoiding the file write keeps this
89 * cheap.
90 */
91 function gutenberg_user_content_types_maybe_flush_rewrite_rules() {
92 if ( ! get_option( GUTENBERG_USER_CONTENT_TYPES_FLUSH_OPTION ) ) {
93 return;
94 }
95 delete_option( GUTENBERG_USER_CONTENT_TYPES_FLUSH_OPTION );
96 flush_rewrite_rules( false );
97 }
98 add_action( 'init', 'gutenberg_user_content_types_maybe_flush_rewrite_rules', 30 );
99
100 /**
101 * Registers the wp_user_taxonomy CPT.
102 */
103 function gutenberg_register_user_taxonomy_cpt() {
104 register_post_type(
105 'wp_user_taxonomy',
106 array(
107 'labels' => array(
108 'name' => __( 'User taxonomies', 'gutenberg' ),
109 'singular_name' => __( 'User taxonomy', 'gutenberg' ),
110 'add_new_item' => __( 'Add taxonomy', 'gutenberg' ),
111 ),
112 'public' => false,
113 'publicly_queryable' => false,
114 'show_ui' => false,
115 'show_in_menu' => false,
116 'show_in_rest' => true,
117 'rest_base' => 'user-taxonomies',
118 'rest_controller_class' => 'WP_REST_User_Taxonomies_Controller_Gutenberg',
119 'capability_type' => 'post',
120 'capabilities' => array(
121 /**
122 * Capability map: every write operation requires `manage_options`.
123 * Read is allowed for any authenticated user that can `edit_posts` so the
124 * REST endpoint can be consumed by the Settings pages without exposing the
125 * records to unauthenticated visitors.
126 */
127 'read' => 'edit_posts',
128 'create_posts' => 'manage_options',
129 'edit_posts' => 'manage_options',
130 'edit_published_posts' => 'manage_options',
131 'delete_posts' => 'manage_options',
132 'delete_published_posts' => 'manage_options',
133 'edit_others_posts' => 'manage_options',
134 'delete_others_posts' => 'manage_options',
135 'publish_posts' => 'manage_options',
136 ),
137 'map_meta_cap' => true,
138 'supports' => array( 'title', 'editor' ),
139 'hierarchical' => false,
140 'has_archive' => false,
141 'rewrite' => false,
142 'query_var' => false,
143 '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
144 )
145 );
146
147 register_post_meta(
148 'wp_user_taxonomy',
149 GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY,
150 array(
151 // One row per attached post type, so `meta_query IN` can filter
152 // listings by individual slug.
153 'single' => false,
154 'type' => 'string',
155 // Surfaced via the REST controller as a top-level `object_type` array.
156 // Setting `show_in_rest => false` keeps the raw meta key out of the
157 // REST response so clients only see the typed field.
158 'show_in_rest' => false,
159 'sanitize_callback' => 'sanitize_key',
160 )
161 );
162 }
163
164 add_action( 'init', 'gutenberg_register_user_taxonomy_cpt' );
165
166 /**
167 * Sanitizes a decoded taxonomy config to the canonical shape declared by
168 * the REST controller's config schema. Single sanitization site for
169 * taxonomy records — called from {@see gutenberg_filter_user_taxonomy_post_content}
170 * on `wp_insert_post_data`.
171 *
172 * @param array $config Raw decoded config.
173 * @return array Sanitized config.
174 */
175 function gutenberg_user_taxonomy_sanitize_config( $config ) {
176 if ( ! is_array( $config ) ) {
177 return array();
178 }
179
180 $clean = rest_sanitize_value_from_schema(
181 $config,
182 WP_REST_User_Taxonomies_Controller_Gutenberg::get_config_schema()
183 );
184 if ( ! is_array( $clean ) ) {
185 return array();
186 }
187
188 // `rest_sanitize_value_from_schema()` casts strings to their declared
189 // type but doesn't strip HTML or control characters, so layer that on.
190 if ( isset( $clean['description'] ) ) {
191 $clean['description'] = sanitize_textarea_field( (string) $clean['description'] );
192 }
193 if ( isset( $clean['labels'] ) && is_array( $clean['labels'] ) ) {
194 foreach ( $clean['labels'] as $key => $value ) {
195 $clean['labels'][ $key ] = sanitize_text_field( (string) $value );
196 }
197 }
198
199 return $clean;
200 }
201
202 /**
203 * Sanitizes wp_user_taxonomy JSON `post_content` during `wp_insert_post`.
204 *
205 * Acts on posts of type `wp_user_taxonomy`. Returns input unchanged for
206 * any other post type. Invalid JSON is normalized to the canonical
207 * marker-only payload rather than passed through. The filter is
208 * unconditional — taxonomy config isn't HTML and shouldn't carry scripts
209 * even for users with `unfiltered_html`.
210 *
211 * @param array $data Slashed post data being inserted/updated.
212 * @return array Filtered data.
213 */
214 function gutenberg_filter_user_taxonomy_post_content( $data ) {
215 if ( ! isset( $data['post_type'], $data['post_content'] ) ) {
216 return $data;
217 }
218
219 if ( 'wp_user_taxonomy' !== $data['post_type'] ) {
220 return $data;
221 }
222
223 $decoded = json_decode( wp_unslash( (string) $data['post_content'] ), true );
224 if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $decoded ) ) {
225 // Hedge: invalid JSON falls through to a canonical empty payload so
226 // a stray read path can't surface arbitrary bytes. The marker is
227 // added below, keeping the stored shape uniform.
228 $decoded = array();
229 }
230
231 $clean = gutenberg_user_taxonomy_sanitize_config( $decoded );
232
233 // Storage-only marker: deliberately not in the REST schema so it can
234 // never reach clients. Kept as a forward-compat anchor for a
235 // content-only fallback sanitizer; full rationale on the const.
236 $clean[ GUTENBERG_USER_TAXONOMY_CONFIG_MARKER ] = true;
237
238 // `wp_insert_post_data` is the last filter before the row is written,
239 // so the re-encode here is what lands in the database.
240 // `JSON_HEX_TAG | JSON_HEX_AMP` guarantee the stored bytes carry no
241 // live `<`, `>`, or `&`, so any subsequent pass through kses (on
242 // later updates or on display) sees an inert string. kses on
243 // `content_save_pre` already ran earlier in `wp_insert_post()`; for
244 // REST writes that input was pre-escaped by
245 // `prepare_item_for_database`, so that earlier pass was also a no-op.
246 $data['post_content'] = wp_slash(
247 wp_json_encode(
248 WP_REST_User_Taxonomies_Controller_Gutenberg::normalize_config_for_encode( $clean ),
249 JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
250 )
251 );
252
253 return $data;
254 }
255 add_filter( 'wp_insert_post_data', 'gutenberg_filter_user_taxonomy_post_content' );
256
257 /**
258 * Reads the stored object_type meta values for a record, filtering down to
259 * post types that currently exist.
260 *
261 * @param int $post_id Record ID.
262 * @return string[]
263 */
264 function gutenberg_user_taxonomy_read_object_type( $post_id ) {
265 $values = get_post_meta( $post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY );
266 if ( ! is_array( $values ) ) {
267 return array();
268 }
269 $out = array();
270 foreach ( $values as $value ) {
271 if ( is_string( $value ) && post_type_exists( $value ) ) {
272 $out[] = $value;
273 }
274 }
275 return array_values( array_unique( $out ) );
276 }
277
278 /**
279 * Appends a post-type slug to a wp_user_taxonomy record's
280 * `_wp_user_taxonomy_object_type` meta if not already present. No-op if
281 * the slug is already attached.
282 *
283 * @param int $tax_post_id wp_user_taxonomy record ID.
284 * @param string $object_type Post type slug to attach.
285 */
286 function gutenberg_user_taxonomy_attach_object_type( $tax_post_id, $object_type ) {
287 if ( 'wp_user_taxonomy' !== get_post_type( $tax_post_id ) ) {
288 return;
289 }
290 $clean = is_string( $object_type ) ? sanitize_key( $object_type ) : '';
291 if ( '' === $clean ) {
292 return;
293 }
294 $existing = (array) get_post_meta( $tax_post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY );
295 if ( in_array( $clean, $existing, true ) ) {
296 return;
297 }
298 add_post_meta( $tax_post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY, $clean );
299 }
300
301 /**
302 * Removes a single post-type slug from a wp_user_taxonomy record's
303 * `_wp_user_taxonomy_object_type` meta. Other slugs remain.
304 *
305 * @param int $tax_post_id wp_user_taxonomy record ID.
306 * @param string $object_type Post type slug to detach.
307 */
308 function gutenberg_user_taxonomy_detach_object_type( $tax_post_id, $object_type ) {
309 if ( 'wp_user_taxonomy' !== get_post_type( $tax_post_id ) ) {
310 return;
311 }
312 $clean = is_string( $object_type ) ? sanitize_key( $object_type ) : '';
313 if ( '' === $clean ) {
314 return;
315 }
316 delete_post_meta( $tax_post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY, $clean );
317 }
318
319 /**
320 * Replaces every `_wp_user_taxonomy_object_type` meta row on a record
321 * with the given list. Each value is sanitized via `sanitize_key()` and
322 * filtered against `post_type_exists()` so only registered post types
323 * land in storage; duplicates are collapsed.
324 *
325 * @param int $tax_post_id wp_user_taxonomy record ID.
326 * @param array<int, string> $object_types Post type slugs to store.
327 */
328 function gutenberg_user_taxonomy_replace_object_types( $tax_post_id, array $object_types ) {
329 $values = array();
330 foreach ( $object_types as $slug ) {
331 if ( ! is_string( $slug ) ) {
332 continue;
333 }
334 $clean = sanitize_key( $slug );
335 if ( '' !== $clean && post_type_exists( $clean ) ) {
336 $values[] = $clean;
337 }
338 }
339 $values = array_values( array_unique( $values ) );
340
341 delete_post_meta( $tax_post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY );
342 foreach ( $values as $slug ) {
343 add_post_meta( $tax_post_id, GUTENBERG_USER_TAXONOMY_OBJECT_TYPE_META_KEY, $slug );
344 }
345 }
346
347 /**
348 * Builds register_taxonomy() arguments from a wp_user_taxonomy record.
349 * Returns null for invalid records so callers can skip them uniformly.
350 *
351 * @param WP_Post $record Stored taxonomy record.
352 * @return array{0: string, 1: string[], 2: array}|null [ $slug, $object_type, $args ].
353 */
354 function gutenberg_build_user_taxonomy_args( WP_Post $record ) {
355 $slug = $record->post_name;
356 if ( ! is_string( $slug ) || ! preg_match( GUTENBERG_USER_TAXONOMY_SLUG_PATTERN, $slug ) ) {
357 return null;
358 }
359
360 $decoded = json_decode( (string) $record->post_content, true, 8 );
361 if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $decoded ) ) {
362 return null;
363 }
364 unset( $decoded[ GUTENBERG_USER_TAXONOMY_CONFIG_MARKER ] );
365 // Storage is sanitized at write-time by the filter on
366 // `wp_insert_post_data`, so we trust the decoded shape here.
367 $config = $decoded;
368
369 $object_type = gutenberg_user_taxonomy_read_object_type( $record->ID );
370
371 $title = sanitize_text_field( $record->post_title );
372 $singular = isset( $config['labels']['singular_name'] )
373 ? (string) $config['labels']['singular_name']
374 : '';
375 $labels = array(
376 'name' => $title,
377 'singular_name' => '' !== $singular ? $singular : $title,
378 );
379
380 // Merge optional label overrides. The sanitizer has already pruned
381 // unknown keys against the schema, so we can trust whatever the stored
382 // labels object contains. Empty strings fall through to the
383 // WordPress-generated defaults.
384 $stored_labels = isset( $config['labels'] ) && is_array( $config['labels'] )
385 ? $config['labels']
386 : array();
387 foreach ( array_keys( $stored_labels ) as $label_key ) {
388 if ( 'singular_name' === $label_key ) {
389 continue;
390 }
391 if ( ! empty( $stored_labels[ $label_key ] ) ) {
392 $labels[ $label_key ] = (string) $stored_labels[ $label_key ];
393 }
394 }
395
396 $args = array(
397 'labels' => $labels,
398 );
399
400 if ( ! empty( $config['description'] ) ) {
401 $args['description'] = (string) $config['description'];
402 }
403
404 $bool_keys = array(
405 'public',
406 'hierarchical',
407 'publicly_queryable',
408 'show_ui',
409 'show_in_menu',
410 'show_in_nav_menus',
411 'show_tagcloud',
412 'show_in_quick_edit',
413 'show_admin_column',
414 'sort',
415 );
416 foreach ( $bool_keys as $key ) {
417 if ( array_key_exists( $key, $config ) ) {
418 $args[ $key ] = (bool) $config[ $key ];
419 }
420 }
421 $args['show_in_rest'] = isset( $config['show_in_rest'] ) ? (bool) $config['show_in_rest'] : true;
422
423 if ( isset( $config['default_term']['name'] ) ) {
424 $default_term_name = sanitize_text_field( (string) $config['default_term']['name'] );
425 if ( '' !== $default_term_name ) {
426 $args['default_term'] = array( 'name' => $default_term_name );
427 }
428 }
429
430 return array( $slug, $object_type, $args );
431 }
432
433 /**
434 * Reads each published wp_user_taxonomy record and calls register_taxonomy()
435 * with a tightly-validated subset of its stored config.
436 */
437 function gutenberg_register_user_defined_taxonomies() {
438 $records = get_posts(
439 array(
440 'post_type' => 'wp_user_taxonomy',
441 // Drafts are skipped so the Edit "Active" toggle gates registration.
442 'post_status' => 'publish',
443 'posts_per_page' => -1,
444 'no_found_rows' => true,
445 'suppress_filters' => true,
446 )
447 );
448
449 foreach ( $records as $record ) {
450 $built = gutenberg_build_user_taxonomy_args( $record );
451 if ( null === $built ) {
452 continue;
453 }
454 list( $slug, $object_type, $args ) = $built;
455
456 // Defense-in-depth: never overwrite an existing taxonomy registration,
457 // even if a bad record slipped past server-side slug validation.
458 if ( taxonomy_exists( $slug ) ) {
459 continue;
460 }
461
462 register_taxonomy( $slug, $object_type, $args );
463 }
464 }
465 // Priority 25 — after gutenberg_register_user_defined_post_types() (priority 20)
466 // so user CPT slugs exist when register_taxonomy() records the object_type association.
467 add_action( 'init', 'gutenberg_register_user_defined_taxonomies', 25 );
468