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 / guidelines / class-gutenberg-content-guidelines-rest-controller.php

class-gutenberg-content-guidelines-rest-controller.php in Gutenberg 23.3.0, at lib/experimental/guidelines/class-gutenberg-content-guidelines-rest-controller.php

824 lines 25.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Content Guidelines REST API Controller.
4 *
5 * Specialized controller for the site-wide "content" guideline singleton.
6 * Exposes a flat `/wp/v2/content-guidelines` endpoint that always reads,
7 * creates, and updates a single post tagged with the `content` term in
8 * the `wp_guideline_type` taxonomy. Other guideline posts (artifacts) are
9 * served by the standard `/wp/v2/guidelines` collection.
10 *
11 * @package gutenberg
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * REST API controller for the site-wide content guidelines singleton.
20 */
21 class Gutenberg_Content_Guidelines_REST_Controller extends WP_REST_Posts_Controller {
22
23 /**
24 * Maximum length for guideline text strings.
25 *
26 * @var int
27 */
28 const MAX_GUIDELINE_LENGTH = 5000;
29
30 /**
31 * Maximum length for category label strings.
32 *
33 * @var int
34 */
35 const MAX_LABEL_LENGTH = 200;
36
37 /**
38 * REST base for the singleton route.
39 *
40 * @var string
41 */
42 const REST_BASE = 'content-guidelines';
43
44 /**
45 * Constructor.
46 */
47 public function __construct() {
48 parent::__construct( Gutenberg_Guidelines_Post_Type::POST_TYPE );
49 $this->rest_base = self::REST_BASE;
50 }
51
52 /**
53 * Resolves a post ID to a content-typed guideline post.
54 *
55 * Restricts /wp/v2/content-guidelines/{id} to posts tagged with the
56 * `content` term. Other guideline types are addressable only via the
57 * standard /wp/v2/guidelines collection.
58 *
59 * @param int $id Post ID.
60 * @return WP_Post|WP_Error Post object on success, WP_Error on failure.
61 */
62 protected function get_post( $id ) {
63 $post = parent::get_post( $id );
64 if ( is_wp_error( $post ) ) {
65 return $post;
66 }
67
68 if ( ! Gutenberg_Guidelines_Post_Type::is_content_guideline( $post->ID ) ) {
69 return new WP_Error(
70 'rest_post_invalid_id',
71 __( 'Invalid post ID.', 'gutenberg' ),
72 array( 'status' => 404 )
73 );
74 }
75
76 return $post;
77 }
78
79 /**
80 * Registers the routes for the content guidelines singleton.
81 *
82 * Calls parent to register standard /{id} CRUD routes, then overrides the
83 * collection route with a singleton GET endpoint.
84 */
85 public function register_routes() {
86 parent::register_routes();
87
88 // Override collection route with singleton GET + create.
89 register_rest_route(
90 $this->namespace,
91 '/' . $this->rest_base,
92 array(
93 array(
94 'methods' => WP_REST_Server::READABLE,
95 'callback' => array( $this, 'get_guidelines' ),
96 'permission_callback' => array( $this, 'get_guidelines_permissions_check' ),
97 'args' => array(
98 'category' => array(
99 'description' => __( 'Limit response to a specific guideline category.', 'gutenberg' ),
100 'type' => 'string',
101 'enum' => Gutenberg_Guidelines_Post_Type::VALID_CATEGORIES,
102 'sanitize_callback' => 'sanitize_text_field',
103 ),
104 'block' => array(
105 'description' => __( 'Limit response to guidelines for a specific block type.', 'gutenberg' ),
106 'type' => 'string',
107 'sanitize_callback' => 'sanitize_text_field',
108 ),
109 'status' => array(
110 'description' => __( 'Limit response to guidelines with a specific status.', 'gutenberg' ),
111 'type' => 'string',
112 'enum' => Gutenberg_Guidelines_Post_Type::VALID_STATUSES,
113 'sanitize_callback' => 'sanitize_text_field',
114 ),
115 ),
116 ),
117 array(
118 'methods' => WP_REST_Server::CREATABLE,
119 'callback' => array( $this, 'create_item' ),
120 'permission_callback' => array( $this, 'create_item_permissions_check' ),
121 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
122 ),
123 'schema' => array( $this, 'get_public_item_schema' ),
124 ),
125 true
126 );
127 }
128
129 /**
130 * Retrieves the query params for the collection.
131 *
132 * Overridden to return empty since we use a singleton pattern, not a collection.
133 *
134 * @return array Empty collection parameters.
135 */
136 public function get_collection_params() {
137 return array();
138 }
139
140 /**
141 * Checks if a given request has access to read the singleton guidelines.
142 *
143 * @param WP_REST_Request $request Full details about the request.
144 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
145 */
146 public function get_guidelines_permissions_check( WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
147 $post_type = get_post_type_object( $this->post_type );
148 if ( ! current_user_can( $post_type->cap->read ) ) {
149 return new WP_Error(
150 'rest_forbidden',
151 __( 'Sorry, you are not allowed to view the guidelines.', 'gutenberg' ),
152 array( 'status' => rest_authorization_required_code() )
153 );
154 }
155
156 return true;
157 }
158
159 /**
160 * Restricts guideline creation to administrators.
161 *
162 * Defers to the parent controller for per-post checks (status validation,
163 * sticky support, etc.) once the admin gate passes.
164 *
165 * @param WP_REST_Request $request Full details about the request.
166 * @return true|WP_Error True if the request has access, WP_Error object otherwise.
167 */
168 public function create_item_permissions_check( $request ) {
169 if ( ! current_user_can( 'manage_options' ) ) {
170 return new WP_Error(
171 'rest_cannot_create',
172 __( 'Sorry, you are not allowed to create guidelines.', 'gutenberg' ),
173 array( 'status' => rest_authorization_required_code() )
174 );
175 }
176
177 return parent::create_item_permissions_check( $request );
178 }
179
180 /**
181 * Restricts guideline updates to administrators.
182 *
183 * @param WP_REST_Request $request Full details about the request.
184 * @return true|WP_Error True if the request has access, WP_Error object otherwise.
185 */
186 public function update_item_permissions_check( $request ) {
187 if ( ! current_user_can( 'manage_options' ) ) {
188 return new WP_Error(
189 'rest_cannot_edit',
190 __( 'Sorry, you are not allowed to edit guidelines.', 'gutenberg' ),
191 array( 'status' => rest_authorization_required_code() )
192 );
193 }
194
195 return parent::update_item_permissions_check( $request );
196 }
197
198 /**
199 * Restricts guideline deletion to administrators.
200 *
201 * @param WP_REST_Request $request Full details about the request.
202 * @return true|WP_Error True if the request has access, WP_Error object otherwise.
203 */
204 public function delete_item_permissions_check( $request ) {
205 if ( ! current_user_can( 'manage_options' ) ) {
206 return new WP_Error(
207 'rest_cannot_delete',
208 __( 'Sorry, you are not allowed to delete guidelines.', 'gutenberg' ),
209 array( 'status' => rest_authorization_required_code() )
210 );
211 }
212
213 return parent::delete_item_permissions_check( $request );
214 }
215
216 /**
217 * Gets the singleton guidelines.
218 *
219 * Supports query parameters:
220 * - ?status=publish|draft - Filter by status
221 * - ?category=copy|images|site|blocks|additional - Return only specific category
222 * - ?block=core/paragraph - Return only specific block's guidelines
223 *
224 * @param WP_REST_Request $request Full details about the request.
225 * @return WP_REST_Response Response object.
226 */
227 public function get_guidelines( WP_REST_Request $request ) {
228 $status_filter = $request->get_param( 'status' );
229 $post = $this->get_guidelines_post( $status_filter );
230
231 if ( ! $post ) {
232 $empty_status = $status_filter ? $status_filter : 'draft';
233 return rest_ensure_response(
234 array(
235 'id' => 0,
236 'status' => $empty_status,
237 'guideline_categories' => new stdClass(),
238 )
239 );
240 }
241
242 return $this->prepare_item_for_response( $post, $request );
243 }
244
245 /**
246 * Creates the content guidelines singleton.
247 *
248 * Enforces the singleton constraint — only one post tagged with the
249 * `content` term may exist.
250 *
251 * @param WP_REST_Request $request Full details about the request.
252 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error on failure.
253 */
254 public function create_item( $request ) {
255 $existing = $this->get_guidelines_post();
256 if ( $existing ) {
257 return new WP_Error(
258 'rest_guidelines_exists',
259 __( 'Guidelines already exist. Use PATCH to update.', 'gutenberg' ),
260 array( 'status' => 400 )
261 );
262 }
263
264 $content_term_id = self::get_or_create_term_id(
265 Gutenberg_Guidelines_Post_Type::TERM_CONTENT,
266 __( 'Content', 'gutenberg' )
267 );
268 if ( is_wp_error( $content_term_id ) ) {
269 return $content_term_id;
270 }
271
272 $prepared = $this->prepare_item_for_database( $request );
273 $prepared->post_type = $this->post_type;
274 $prepared->post_title = __( 'Guidelines', 'gutenberg' );
275 $prepared->tax_input = array(
276 Gutenberg_Guidelines_Post_Type::TAXONOMY => array( $content_term_id ),
277 );
278
279 if ( ! isset( $prepared->post_status ) ) {
280 $prepared->post_status = 'draft';
281 }
282
283 $post_id = wp_insert_post( wp_slash( (array) $prepared ), true );
284
285 if ( is_wp_error( $post_id ) ) {
286 return $post_id;
287 }
288
289 if ( isset( $request['guideline_categories'] ) ) {
290 $categories = $this->sanitize_guideline_categories( $request['guideline_categories'] );
291 $this->save_guideline_categories_to_meta( $post_id, $categories );
292 }
293
294 $post = get_post( $post_id );
295
296 $request->set_param( 'context', 'edit' );
297 $response = $this->prepare_item_for_response( $post, $request );
298 $response = rest_ensure_response( $response );
299 $response->set_status( 201 );
300 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
301
302 return $response;
303 }
304
305 /**
306 * Updates the content guidelines singleton.
307 *
308 * Saves guideline categories to meta before updating the post so that
309 * the revision captures the updated meta values.
310 *
311 * @param WP_REST_Request $request Full details about the request.
312 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error on failure.
313 */
314 public function update_item( $request ) {
315 $post = $this->get_post( $request['id'] );
316 if ( is_wp_error( $post ) ) {
317 return $post;
318 }
319
320 // Save guideline categories to meta first (so revision captures them).
321 if ( isset( $request['guideline_categories'] ) ) {
322 $categories = $this->sanitize_guideline_categories( $request['guideline_categories'] );
323 $this->save_guideline_categories_to_meta( $post->ID, $categories );
324 }
325
326 $prepared = $this->prepare_item_for_database( $request );
327 $prepared->ID = $post->ID;
328
329 // Trigger a post update to create a revision with the meta changes.
330 $result = wp_update_post( wp_slash( (array) $prepared ), true );
331
332 if ( is_wp_error( $result ) ) {
333 return $result;
334 }
335
336 $post = get_post( $post->ID );
337
338 $request->set_param( 'context', 'edit' );
339
340 return $this->prepare_item_for_response( $post, $request );
341 }
342
343 /**
344 * Prepares a single guidelines post for database.
345 *
346 * Returns a stdClass with standard post fields. Guideline categories
347 * are handled separately via save_guideline_categories_to_meta().
348 *
349 * @param WP_REST_Request $request Request object.
350 * @return stdClass Prepared post data.
351 */
352 protected function prepare_item_for_database( $request ) {
353 $prepared = new stdClass();
354
355 if ( isset( $request['id'] ) ) {
356 $prepared->ID = $request['id'];
357 }
358
359 if ( isset( $request['status'] ) ) {
360 $prepared->post_status = $request['status'];
361 }
362
363 return $prepared;
364 }
365
366 /**
367 * Prepares a single guidelines output for response.
368 *
369 * Builds the guideline_categories structured response from post meta
370 * and includes standard _links.
371 *
372 * @param WP_Post $post Post object.
373 * @param WP_REST_Request $request Request object.
374 * @return WP_REST_Response Response object.
375 */
376 public function prepare_item_for_response( $post, $request ) {
377 $fields = $this->get_fields_for_response( $request );
378 $data = array();
379
380 if ( rest_is_field_included( 'id', $fields ) ) {
381 $data['id'] = $post->ID;
382 }
383
384 if ( rest_is_field_included( 'status', $fields ) ) {
385 $data['status'] = $post->post_status;
386 }
387
388 if ( rest_is_field_included( 'guideline_categories', $fields ) ) {
389 $guideline_categories = Gutenberg_Guidelines_Post_Type::get_guideline_categories_from_meta( $post->ID );
390
391 // Handle ?block filter.
392 $block_filter = $request->get_param( 'block' );
393 if ( $block_filter && ! empty( $guideline_categories ) ) {
394 if ( isset( $guideline_categories['blocks'][ $block_filter ] ) ) {
395 $guideline_categories = array(
396 'blocks' => array(
397 $block_filter => $guideline_categories['blocks'][ $block_filter ],
398 ),
399 );
400 } else {
401 $guideline_categories = new stdClass();
402 }
403 } elseif ( $request->get_param( 'category' ) ) {
404 // Handle ?category filter.
405 $category_filter = $request->get_param( 'category' );
406 if ( isset( $guideline_categories[ $category_filter ] ) ) {
407 $guideline_categories = array(
408 $category_filter => $guideline_categories[ $category_filter ],
409 );
410 } else {
411 $guideline_categories = new stdClass();
412 }
413 }
414
415 if ( empty( $guideline_categories ) ) {
416 $guideline_categories = new stdClass();
417 }
418
419 $data['guideline_categories'] = $guideline_categories;
420 }
421
422 if ( rest_is_field_included( 'date', $fields ) ) {
423 $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
424 }
425
426 if ( rest_is_field_included( 'date_gmt', $fields ) ) {
427 $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
428 }
429
430 if ( rest_is_field_included( 'modified', $fields ) ) {
431 $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
432 }
433
434 if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
435 $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
436 }
437
438 if ( rest_is_field_included( 'author', $fields ) ) {
439 $data['author'] = (int) $post->post_author;
440 }
441
442 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
443 $data = $this->add_additional_fields_to_object( $data, $request );
444 $data = $this->filter_response_by_context( $data, $context );
445
446 $response = rest_ensure_response( $data );
447
448 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
449 $response->add_links( $this->prepare_links( $post->ID ) );
450 }
451
452 return $response;
453 }
454
455 /**
456 * Prepares links for the request.
457 *
458 * Includes self, about, and version-history links.
459 *
460 * @param int $id Post ID.
461 * @return array Links for the given post.
462 */
463 protected function prepare_links( $id ) {
464 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
465
466 $links = array(
467 'self' => array(
468 'href' => rest_url( trailingslashit( $base ) . $id ),
469 ),
470 'about' => array(
471 'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
472 ),
473 );
474
475 if ( post_type_supports( $this->post_type, 'revisions' ) ) {
476 $revisions = wp_get_latest_revision_id_and_total_count( $id );
477 $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
478 $revisions_base = sprintf( '/%s/%d/revisions', $base, $id );
479
480 $links['version-history'] = array(
481 'href' => rest_url( $revisions_base ),
482 'count' => $revisions_count,
483 );
484 }
485
486 return $links;
487 }
488
489 /**
490 * Saves guideline categories to post meta.
491 *
492 * @param int $post_id Post ID.
493 * @param array $categories Sanitized guideline categories.
494 */
495 protected function save_guideline_categories_to_meta( int $post_id, array $categories ): void {
496 // Save standard categories.
497 foreach ( Gutenberg_Guidelines_Post_Type::CATEGORY_META_KEYS as $category ) {
498 if ( isset( $categories[ $category ] ) ) {
499 $meta_key = '_guideline_' . $category;
500 $value = $categories[ $category ]['guidelines'] ?? '';
501 update_post_meta( $post_id, $meta_key, $value );
502 }
503 }
504
505 // Handle block-specific guidelines as individual meta keys.
506 if ( isset( $categories['blocks'] ) && is_array( $categories['blocks'] ) ) {
507 foreach ( $categories['blocks'] as $block_name => $block_data ) {
508 $meta_key = Gutenberg_Guidelines_Post_Type::block_name_to_meta_key( $block_name );
509 $value = $block_data['guidelines'] ?? '';
510
511 if ( ! empty( $value ) ) {
512 update_post_meta( $post_id, $meta_key, $value );
513 } else {
514 delete_post_meta( $post_id, $meta_key );
515 }
516 }
517 }
518 }
519
520 /**
521 * Sanitizes guideline categories data.
522 *
523 * @param mixed $categories Raw guideline categories from the request.
524 * @return array Sanitized guideline categories.
525 */
526 protected function sanitize_guideline_categories( $categories ): array {
527 if ( ! is_array( $categories ) ) {
528 return array();
529 }
530
531 $valid_categories = Gutenberg_Guidelines_Post_Type::VALID_CATEGORIES;
532 $sanitized = array_intersect_key( $categories, array_flip( $valid_categories ) );
533
534 foreach ( $sanitized as $key => &$category ) {
535 if ( ! is_array( $category ) ) {
536 unset( $sanitized[ $key ] );
537 continue;
538 }
539
540 if ( 'blocks' === $key ) {
541 $category = $this->sanitize_blocks_category( $category );
542 } else {
543 $category = $this->sanitize_standard_category( $category );
544 }
545 }
546 unset( $category );
547
548 return $sanitized;
549 }
550
551 /**
552 * Sanitizes a standard (non-blocks) guideline category.
553 *
554 * @param array $category Raw category data.
555 * @return array Sanitized category data.
556 */
557 private function sanitize_standard_category( array $category ): array {
558 $sanitized = array_intersect_key( $category, array_flip( array( 'label', 'guidelines' ) ) );
559
560 foreach ( $sanitized as $key => &$value ) {
561 $value = is_string( $value ) ? sanitize_textarea_field( $value ) : '';
562 $max = 'label' === $key ? self::MAX_LABEL_LENGTH : self::MAX_GUIDELINE_LENGTH;
563 if ( mb_strlen( $value, 'UTF-8' ) > $max ) {
564 $value = mb_substr( $value, 0, $max, 'UTF-8' );
565 }
566 }
567 unset( $value );
568
569 return $sanitized;
570 }
571
572 /**
573 * Sanitizes the blocks guideline category.
574 *
575 * @param array $blocks Raw blocks category data.
576 * @return array Sanitized blocks category data.
577 */
578 private function sanitize_blocks_category( array $blocks ): array {
579 $sanitized = array();
580
581 foreach ( $blocks as $block_name => $block_data ) {
582 // Matches the block name validation in WP_Block_Type_Registry::register().
583 if ( ! is_string( $block_name ) || ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $block_name ) ) {
584 continue;
585 }
586
587 if ( ! is_array( $block_data ) ) {
588 continue;
589 }
590
591 $sanitized_block = array_intersect_key( $block_data, array_flip( array( 'guidelines' ) ) );
592
593 if ( isset( $sanitized_block['guidelines'] ) ) {
594 $sanitized_block['guidelines'] = is_string( $sanitized_block['guidelines'] )
595 ? sanitize_textarea_field( $sanitized_block['guidelines'] )
596 : '';
597 if ( mb_strlen( $sanitized_block['guidelines'], 'UTF-8' ) > self::MAX_GUIDELINE_LENGTH ) {
598 $sanitized_block['guidelines'] = mb_substr( $sanitized_block['guidelines'], 0, self::MAX_GUIDELINE_LENGTH, 'UTF-8' );
599 }
600 }
601
602 $sanitized[ $block_name ] = $sanitized_block;
603 }
604
605 return $sanitized;
606 }
607
608 /**
609 * Gets the single content guidelines post.
610 *
611 * @param string|null $status_filter Optional. Filter by status ('publish' or 'draft').
612 * @return WP_Post|null The guidelines post or null if not found.
613 */
614 protected function get_guidelines_post( ?string $status_filter = null ): ?WP_Post {
615 $post_status = array( 'publish', 'draft' );
616
617 if ( $status_filter ) {
618 $post_status = $status_filter;
619 }
620
621 $posts = get_posts(
622 array(
623 'post_type' => $this->post_type,
624 'post_status' => $post_status,
625 'posts_per_page' => 1,
626 'orderby' => 'date',
627 'order' => 'DESC',
628 'no_found_rows' => true,
629 'tax_query' => array(
630 array(
631 'taxonomy' => Gutenberg_Guidelines_Post_Type::TAXONOMY,
632 'field' => 'slug',
633 'terms' => Gutenberg_Guidelines_Post_Type::TERM_CONTENT,
634 ),
635 ),
636 )
637 );
638
639 return ! empty( $posts ) ? $posts[0] : null;
640 }
641
642 /**
643 * Retrieves the guidelines schema, conforming to JSON Schema.
644 *
645 * @return array Item schema data.
646 */
647 public function get_item_schema() {
648 if ( $this->schema ) {
649 return $this->add_additional_fields_schema( $this->schema );
650 }
651
652 $this->schema = array(
653 '$schema' => 'http://json-schema.org/draft-04/schema#',
654 'title' => 'content-guidelines',
655 'type' => 'object',
656 'properties' => array(
657 'id' => array(
658 'description' => __( 'Unique identifier for the guidelines.', 'gutenberg' ),
659 'type' => 'integer',
660 'context' => array( 'view', 'edit' ),
661 'readonly' => true,
662 ),
663 'status' => array(
664 'description' => __( 'The status of the guidelines (draft or publish).', 'gutenberg' ),
665 'type' => 'string',
666 'enum' => Gutenberg_Guidelines_Post_Type::VALID_STATUSES,
667 'context' => array( 'view', 'edit' ),
668 ),
669 'guideline_categories' => array(
670 'description' => __( 'The guideline categories and their content.', 'gutenberg' ),
671 'type' => 'object',
672 'context' => array( 'view', 'edit' ),
673 'arg_options' => array(
674 'validate_callback' => static function ( $value ) {
675 if ( ! is_array( $value ) && ! is_object( $value ) ) {
676 return new WP_Error(
677 'rest_invalid_param',
678 __( 'guideline_categories must be a JSON object.', 'gutenberg' ),
679 array( 'status' => 400 )
680 );
681 }
682 return true;
683 },
684 'sanitize_callback' => static function ( $value ) {
685 return (array) $value;
686 },
687 ),
688 'properties' => array(
689 'copy' => array(
690 'type' => 'object',
691 'properties' => array(
692 'label' => array(
693 'type' => 'string',
694 'maxLength' => self::MAX_LABEL_LENGTH,
695 ),
696 'guidelines' => array(
697 'type' => 'string',
698 'maxLength' => self::MAX_GUIDELINE_LENGTH,
699 ),
700 ),
701 ),
702 'images' => array(
703 'type' => 'object',
704 'properties' => array(
705 'label' => array(
706 'type' => 'string',
707 'maxLength' => self::MAX_LABEL_LENGTH,
708 ),
709 'guidelines' => array(
710 'type' => 'string',
711 'maxLength' => self::MAX_GUIDELINE_LENGTH,
712 ),
713 ),
714 ),
715 'site' => array(
716 'type' => 'object',
717 'properties' => array(
718 'label' => array(
719 'type' => 'string',
720 'maxLength' => self::MAX_LABEL_LENGTH,
721 ),
722 'guidelines' => array(
723 'type' => 'string',
724 'maxLength' => self::MAX_GUIDELINE_LENGTH,
725 ),
726 ),
727 ),
728 'blocks' => array(
729 'type' => 'object',
730 'additionalProperties' => array(
731 'type' => 'object',
732 'properties' => array(
733 'guidelines' => array(
734 'type' => 'string',
735 'maxLength' => self::MAX_GUIDELINE_LENGTH,
736 ),
737 ),
738 ),
739 ),
740 'additional' => array(
741 'type' => 'object',
742 'properties' => array(
743 'label' => array(
744 'type' => 'string',
745 'maxLength' => self::MAX_LABEL_LENGTH,
746 ),
747 'guidelines' => array(
748 'type' => 'string',
749 'maxLength' => self::MAX_GUIDELINE_LENGTH,
750 ),
751 ),
752 ),
753 ),
754 ),
755 'date' => array(
756 'description' => __( 'The date the guidelines were created, in the site\'s timezone.', 'gutenberg' ),
757 'type' => 'string',
758 'format' => 'date-time',
759 'context' => array( 'view', 'edit' ),
760 'readonly' => true,
761 ),
762 'date_gmt' => array(
763 'description' => __( 'The date the guidelines were created, as GMT.', 'gutenberg' ),
764 'type' => 'string',
765 'format' => 'date-time',
766 'context' => array( 'view', 'edit' ),
767 'readonly' => true,
768 ),
769 'modified' => array(
770 'description' => __( 'The date the guidelines were last modified, in the site\'s timezone.', 'gutenberg' ),
771 'type' => 'string',
772 'format' => 'date-time',
773 'context' => array( 'view', 'edit' ),
774 'readonly' => true,
775 ),
776 'modified_gmt' => array(
777 'description' => __( 'The date the guidelines were last modified, as GMT.', 'gutenberg' ),
778 'type' => 'string',
779 'format' => 'date-time',
780 'context' => array( 'view', 'edit' ),
781 'readonly' => true,
782 ),
783 'author' => array(
784 'description' => __( 'The ID of the author of the guidelines.', 'gutenberg' ),
785 'type' => 'integer',
786 'context' => array( 'view', 'edit' ),
787 'readonly' => true,
788 ),
789 ),
790 );
791
792 return $this->add_additional_fields_schema( $this->schema );
793 }
794
795 /**
796 * Resolve the `wp_guideline_type` term by slug, creating it if missing.
797 *
798 * Used by the create flow to attach the freshly-inserted content guideline
799 * to the `content` term on first use, before the term is otherwise needed.
800 *
801 * @param string $slug Term slug.
802 * @param string $name Human-readable term name, used when creating.
803 * @return int|WP_Error Term ID on success, WP_Error on failure.
804 */
805 private static function get_or_create_term_id( string $slug, string $name ) {
806 $term = get_term_by( 'slug', $slug, Gutenberg_Guidelines_Post_Type::TAXONOMY );
807 if ( $term ) {
808 return (int) $term->term_id;
809 }
810
811 $inserted = wp_insert_term(
812 $name,
813 Gutenberg_Guidelines_Post_Type::TAXONOMY,
814 array( 'slug' => $slug )
815 );
816
817 if ( is_wp_error( $inserted ) ) {
818 return $inserted;
819 }
820
821 return (int) $inserted['term_id'];
822 }
823 }
824