PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.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 All 404 releases
gutenberg / lib / compat / wordpress-7.1 / class-gutenberg-rest-view-config-controller-7-1.php

class-gutenberg-rest-view-config-controller-7-1.php in Gutenberg trunk, at lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php

812 lines 21.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API: Gutenberg_REST_View_Config_Controller_7_1 class
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Controller which provides a REST endpoint for retrieving the default
10 * view configuration for a given entity type.
11 *
12 * @since 7.1.0
13 */
14 class Gutenberg_REST_View_Config_Controller_7_1 extends WP_REST_Controller {
15
16 /**
17 * Constructor.
18 */
19 public function __construct() {
20 $this->namespace = 'wp/v2';
21 $this->rest_base = 'view-config';
22 }
23
24 /**
25 * Registers the routes for the controller.
26 */
27 public function register_routes() {
28 register_rest_route(
29 $this->namespace,
30 '/' . $this->rest_base,
31 array(
32 array(
33 'methods' => WP_REST_Server::READABLE,
34 'callback' => array( $this, 'get_items' ),
35 'permission_callback' => array( $this, 'get_items_permissions_check' ),
36 'args' => array(
37 'kind' => array(
38 'description' => __( 'Entity kind.', 'gutenberg' ),
39 'type' => 'string',
40 'required' => true,
41 ),
42 'name' => array(
43 'description' => __( 'Entity name.', 'gutenberg' ),
44 'type' => 'string',
45 'required' => true,
46 ),
47 ),
48 ),
49 'schema' => array( $this, 'get_public_item_schema' ),
50 ),
51 true // override existing route defined by core, if it exists
52 );
53 }
54
55 /**
56 * Checks if a given request has access to read view config.
57 *
58 * @param WP_REST_Request $request Full details about the request.
59 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
60 */
61 public function get_items_permissions_check( $request ) {
62 $kind = $request->get_param( 'kind' );
63 $name = $request->get_param( 'name' );
64
65 $capability = $this->get_required_capability( $kind, $name );
66
67 if ( null === $capability ) {
68 return new WP_Error(
69 'rest_view_config_invalid_entity',
70 __( 'Invalid entity kind or name.', 'gutenberg' ),
71 array( 'status' => 404 )
72 );
73 }
74
75 if ( ! current_user_can( $capability ) ) {
76 return new WP_Error(
77 'rest_cannot_read',
78 __( 'Sorry, you are not allowed to read view config.', 'gutenberg' ),
79 array( 'status' => rest_authorization_required_code() )
80 );
81 }
82
83 return true;
84 }
85
86 /**
87 * Resolves the capability required to read the view config for an entity.
88 *
89 * Known kinds map to the capability that gates managing that entity's list:
90 * post types use their own `edit_posts` capability (which honors custom
91 * `capability_type` registrations), taxonomies use `manage_terms`, and
92 * root-level entities use `manage_options`. A post type or taxonomy that is
93 * not registered, or not exposed to the REST API, resolves to `null` so the
94 * request is treated as referencing an unknown entity.
95 *
96 * Any other kind falls back to `edit_posts`. This keeps entities registered
97 * through the `get_entity_view_config_{$kind}_{$name}` filter readable behind
98 * a baseline capability.
99 *
100 * @param string $kind The entity kind (e.g. `postType`).
101 * @param string $name The entity name (e.g. `page`).
102 * @return string|null Capability required to read the config, or null if the
103 * entity is not registered.
104 */
105 protected function get_required_capability( $kind, $name ) {
106 switch ( $kind ) {
107 case 'postType':
108 $post_type = get_post_type_object( $name );
109 if ( $post_type && $post_type->show_in_rest ) {
110 return $post_type->cap->edit_posts;
111 }
112 return null;
113
114 case 'taxonomy':
115 $taxonomy = get_taxonomy( $name );
116 if ( $taxonomy && $taxonomy->show_in_rest ) {
117 return $taxonomy->cap->manage_terms;
118 }
119 return null;
120
121 case 'root':
122 return 'manage_options';
123 }
124
125 return 'edit_posts';
126 }
127
128 /**
129 * Returns the default view configuration for the given entity type.
130 *
131 * @param WP_REST_Request $request Full details about the request.
132 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
133 */
134 public function get_items( $request ) {
135 $kind = $request->get_param( 'kind' );
136 $name = $request->get_param( 'name' );
137
138 $config = gutenberg_get_entity_view_config( $kind, $name );
139 $schema = $this->get_item_schema();
140
141 $response = array(
142 'kind' => $kind,
143 'name' => $name,
144 'version' => Gutenberg_View_Config_Data::LATEST_VERSION,
145 'default_view' => $this->cast_empty_objects( $config['default_view'], $schema['properties']['default_view'] ),
146 'default_layouts' => $this->cast_empty_objects( $config['default_layouts'], $schema['properties']['default_layouts'] ),
147 'view_list' => $this->cast_empty_objects( $config['view_list'], $schema['properties']['view_list'] ),
148 'form' => $this->cast_empty_objects( $config['form'], $schema['properties']['form'] ),
149 );
150
151 return rest_ensure_response( $response );
152 }
153
154 /**
155 * Recursively casts empty arrays to objects where the schema types them as
156 * objects.
157 *
158 * PHP cannot distinguish an empty associative array from an empty list, so
159 * `json_encode()` always serializes `array()` as a JSON array (`[]`). The
160 * REST schema, however, types several values as objects, which must encode
161 * as `{}`. This walks the value against its schema and casts any empty,
162 * object-typed array to an object. Non-empty associative arrays already
163 * encode as objects, so they are left as arrays and only recursed into to
164 * fix any nested empty objects.
165 *
166 * Union schemas (`oneOf`/`anyOf`) are handled only for the empty-array case:
167 * an empty value is cast to an object when any branch allows an object. Such
168 * values are not recursed into, which is sufficient for the form schema
169 * where they never contain empty nested objects.
170 *
171 * @param mixed $value The value to normalize.
172 * @param array $schema The schema node describing the value.
173 * @return mixed The normalized value, with empty object-typed arrays cast to objects.
174 */
175 protected function cast_empty_objects( $value, $schema ) {
176 if ( ! is_array( $value ) || ! is_array( $schema ) ) {
177 return $value;
178 }
179
180 if ( isset( $schema['oneOf'] ) || isset( $schema['anyOf'] ) ) {
181 $branches = isset( $schema['oneOf'] ) ? $schema['oneOf'] : $schema['anyOf'];
182 if ( array() === $value ) {
183 foreach ( $branches as $branch ) {
184 if ( is_array( $branch ) && in_array( 'object', (array) ( isset( $branch['type'] ) ? $branch['type'] : array() ), true ) ) {
185 return (object) array();
186 }
187 }
188 }
189 return $value;
190 }
191
192 $types = (array) ( isset( $schema['type'] ) ? $schema['type'] : array() );
193
194 if ( in_array( 'array', $types, true ) && isset( $schema['items'] ) ) {
195 foreach ( $value as $index => $item ) {
196 $value[ $index ] = $this->cast_empty_objects( $item, $schema['items'] );
197 }
198 return $value;
199 }
200
201 if ( in_array( 'object', $types, true ) ) {
202 if ( isset( $schema['properties'] ) ) {
203 foreach ( $schema['properties'] as $property => $property_schema ) {
204 if ( array_key_exists( $property, $value ) ) {
205 $value[ $property ] = $this->cast_empty_objects( $value[ $property ], $property_schema );
206 }
207 }
208 }
209 if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
210 foreach ( $value as $key => $item ) {
211 if ( isset( $schema['properties'][ $key ] ) ) {
212 continue;
213 }
214 $value[ $key ] = $this->cast_empty_objects( $item, $schema['additionalProperties'] );
215 }
216 }
217
218 // Empty object-typed arrays must serialize as {} to match the schema.
219 if ( array() === $value ) {
220 return (object) array();
221 }
222 }
223
224 return $value;
225 }
226
227 /**
228 * Retrieves the item's schema, conforming to JSON Schema.
229 *
230 * @return array Item schema data.
231 */
232 public function get_item_schema() {
233 if ( $this->schema ) {
234 return $this->add_additional_fields_schema( $this->schema );
235 }
236
237 $view_base_properties = $this->get_view_base_schema();
238
239 $this->schema = array(
240 '$schema' => 'http://json-schema.org/draft-04/schema#',
241 'title' => 'view-config',
242 'type' => 'object',
243 'properties' => array(
244 'kind' => array(
245 'description' => __( 'Entity kind.', 'gutenberg' ),
246 'type' => 'string',
247 'readonly' => true,
248 ),
249 'name' => array(
250 'description' => __( 'Entity name.', 'gutenberg' ),
251 'type' => 'string',
252 'readonly' => true,
253 ),
254 'version' => array(
255 'description' => __( 'The schema version of the configuration.', 'gutenberg' ),
256 'type' => 'integer',
257 'readonly' => true,
258 ),
259 'default_view' => array(
260 'description' => __( 'Default view configuration.', 'gutenberg' ),
261 'type' => 'object',
262 'readonly' => true,
263 'properties' => array_merge(
264 array(
265 'type' => array(
266 'type' => 'string',
267 ),
268 'layout' => $this->get_combined_layout_schema(),
269 ),
270 $view_base_properties
271 ),
272 ),
273 'default_layouts' => array(
274 'description' => __( 'Default layout configurations.', 'gutenberg' ),
275 'type' => 'object',
276 'readonly' => true,
277 'properties' => array(
278 'table' => array(
279 'type' => 'object',
280 'properties' => array_merge(
281 $view_base_properties,
282 array(
283 'layout' => $this->get_table_layout_schema(),
284 )
285 ),
286 ),
287 'list' => array(
288 'type' => 'object',
289 'properties' => array_merge(
290 $view_base_properties,
291 array(
292 'layout' => $this->get_list_layout_schema(),
293 )
294 ),
295 ),
296 'grid' => array(
297 'type' => 'object',
298 'properties' => array_merge(
299 $view_base_properties,
300 array(
301 'layout' => $this->get_grid_layout_schema(),
302 )
303 ),
304 ),
305 'activity' => array(
306 'type' => 'object',
307 'properties' => array_merge(
308 $view_base_properties,
309 array(
310 'layout' => $this->get_list_layout_schema(),
311 )
312 ),
313 ),
314 'pickerGrid' => array(
315 'type' => 'object',
316 'properties' => array_merge(
317 $view_base_properties,
318 array(
319 'layout' => $this->get_grid_layout_schema(),
320 )
321 ),
322 ),
323 'pickerTable' => array(
324 'type' => 'object',
325 'properties' => array_merge(
326 $view_base_properties,
327 array(
328 'layout' => $this->get_table_layout_schema(),
329 )
330 ),
331 ),
332 ),
333 ),
334 'view_list' => array(
335 'description' => __( 'List of default views.', 'gutenberg' ),
336 'type' => 'array',
337 'readonly' => true,
338 'items' => array(
339 'type' => 'object',
340 'properties' => array(
341 'title' => array(
342 'type' => 'string',
343 ),
344 'slug' => array(
345 'type' => 'string',
346 ),
347 'view' => array(
348 'type' => 'object',
349 'properties' => array_merge(
350 array(
351 'type' => array(
352 'type' => 'string',
353 ),
354 'layout' => $this->get_combined_layout_schema(),
355 ),
356 $view_base_properties
357 ),
358 ),
359 ),
360 ),
361 ),
362 'form' => array(
363 'description' => __( 'Default form configuration.', 'gutenberg' ),
364 'type' => 'object',
365 'readonly' => true,
366 'properties' => $this->get_form_schema(),
367 ),
368 ),
369 );
370
371 return $this->add_additional_fields_schema( $this->schema );
372 }
373
374 /**
375 * Returns the schema properties shared by all view types (ViewBase), excluding 'type'.
376 *
377 * Note that `search` and `page` are not part of the schema: they are managed
378 * via the URL, which is their only source of truth.
379 *
380 * @return array Schema properties for the base view configuration.
381 */
382 protected function get_view_base_schema() {
383 return array(
384 'filters' => array(
385 'type' => 'array',
386 'items' => array(
387 'type' => 'object',
388 'properties' => array(
389 'field' => array(
390 'type' => 'string',
391 ),
392 'operator' => array(
393 'type' => 'string',
394 'enum' => array(
395 'is',
396 'isNot',
397 'isAny',
398 'isNone',
399 'isAll',
400 'isNotAll',
401 'lessThan',
402 'greaterThan',
403 'lessThanOrEqual',
404 'greaterThanOrEqual',
405 'before',
406 'after',
407 ),
408 ),
409 'value' => array(),
410 'isLocked' => array(
411 'type' => 'boolean',
412 ),
413 ),
414 ),
415 ),
416 'sort' => array(
417 'type' => 'object',
418 'properties' => array(
419 'field' => array(
420 'type' => 'string',
421 ),
422 'direction' => array(
423 'type' => 'string',
424 'enum' => array( 'asc', 'desc' ),
425 ),
426 ),
427 ),
428 'perPage' => array(
429 'type' => 'integer',
430 ),
431 'fields' => array(
432 'type' => 'array',
433 'items' => array(
434 'type' => 'string',
435 ),
436 ),
437 'titleField' => array(
438 'type' => 'string',
439 ),
440 'mediaField' => array(
441 'type' => 'string',
442 ),
443 'descriptionField' => array(
444 'type' => 'string',
445 ),
446 'showTitle' => array(
447 'type' => 'boolean',
448 ),
449 'showMedia' => array(
450 'type' => 'boolean',
451 ),
452 'showDescription' => array(
453 'type' => 'boolean',
454 ),
455 'showLevels' => array(
456 'type' => 'boolean',
457 ),
458 'groupBy' => array(
459 'type' => 'object',
460 'properties' => array(
461 'field' => array(
462 'type' => 'string',
463 ),
464 'direction' => array(
465 'type' => 'string',
466 'enum' => array( 'asc', 'desc' ),
467 ),
468 'showLabel' => array(
469 'type' => 'boolean',
470 'default' => true,
471 ),
472 ),
473 ),
474 'infiniteScrollEnabled' => array(
475 'type' => 'boolean',
476 ),
477 );
478 }
479
480 /**
481 * Returns the schema for the ColumnStyle type.
482 *
483 * @return array Schema for a column style object.
484 */
485 protected function get_column_style_schema() {
486 return array(
487 'type' => 'object',
488 'properties' => array(
489 'width' => array(
490 'type' => array( 'string', 'number' ),
491 ),
492 'maxWidth' => array(
493 'type' => array( 'string', 'number' ),
494 ),
495 'minWidth' => array(
496 'type' => array( 'string', 'number' ),
497 ),
498 'align' => array(
499 'type' => 'string',
500 'enum' => array( 'start', 'center', 'end' ),
501 ),
502 ),
503 );
504 }
505
506 /**
507 * Returns the layout schema for table-type views (ViewTable, ViewPickerTable).
508 *
509 * @return array Schema for a table layout object.
510 */
511 protected function get_table_layout_schema() {
512 return array(
513 'type' => 'object',
514 'properties' => array(
515 'styles' => array(
516 'type' => 'object',
517 'additionalProperties' => $this->get_column_style_schema(),
518 ),
519 'density' => array(
520 'type' => 'string',
521 'enum' => array( 'compact', 'balanced', 'comfortable' ),
522 ),
523 'enableMoving' => array(
524 'type' => 'boolean',
525 ),
526 ),
527 );
528 }
529
530 /**
531 * Returns the layout schema for list-type views (ViewList, ViewActivity).
532 *
533 * @return array Schema for a list layout object.
534 */
535 protected function get_list_layout_schema() {
536 return array(
537 'type' => 'object',
538 'properties' => array(
539 'density' => array(
540 'type' => 'string',
541 'enum' => array( 'compact', 'balanced', 'comfortable' ),
542 ),
543 ),
544 );
545 }
546
547 /**
548 * Returns a combined layout schema that accepts properties from all view types.
549 *
550 * This is useful for contexts where the view type is not known ahead of time
551 * (e.g. the `view` override in a view list item), so all possible layout
552 * properties must be accepted.
553 *
554 * @return array Schema for a combined layout object.
555 */
556 protected function get_combined_layout_schema() {
557 return array(
558 'type' => 'object',
559 'properties' => array_merge(
560 $this->get_table_layout_schema()['properties'],
561 $this->get_grid_layout_schema()['properties'],
562 $this->get_list_layout_schema()['properties']
563 ),
564 );
565 }
566
567 /**
568 * Returns the layout schema for grid-type views (ViewGrid, ViewPickerGrid).
569 *
570 * @return array Schema for a grid layout object.
571 */
572 protected function get_grid_layout_schema() {
573 return array(
574 'type' => 'object',
575 'properties' => array(
576 'badgeFields' => array(
577 'type' => 'array',
578 'items' => array(
579 'type' => 'string',
580 ),
581 ),
582 'previewSize' => array(
583 'type' => 'number',
584 ),
585 'density' => array(
586 'type' => 'string',
587 'enum' => array( 'compact', 'balanced', 'comfortable' ),
588 ),
589 ),
590 );
591 }
592
593 /**
594 * Returns the schema for a form layout object as a discriminated union.
595 *
596 * Each variant is discriminated by a single-value enum on its `type` property,
597 * matching the TypeScript Layout union in dataviews/src/types/dataform.ts.
598 *
599 * @return array Schema for a form layout object.
600 */
601 protected function get_form_layout_schema() {
602 return array(
603 'oneOf' => array(
604 // RegularLayout.
605 array(
606 'type' => 'object',
607 'properties' => array(
608 'type' => array(
609 'type' => 'string',
610 'enum' => array( 'regular' ),
611 ),
612 'labelPosition' => array(
613 'type' => 'string',
614 'enum' => array( 'top', 'side', 'none' ),
615 ),
616 ),
617 ),
618 // PanelLayout.
619 array(
620 'type' => 'object',
621 'properties' => array(
622 'type' => array(
623 'type' => 'string',
624 'enum' => array( 'panel' ),
625 ),
626 'labelPosition' => array(
627 'type' => 'string',
628 'enum' => array( 'top', 'side', 'none' ),
629 ),
630 'openAs' => array(
631 'oneOf' => array(
632 array(
633 'type' => 'string',
634 'enum' => array( 'dropdown', 'modal' ),
635 ),
636 array(
637 'type' => 'object',
638 'properties' => array(
639 'type' => array(
640 'type' => 'string',
641 'enum' => array( 'dropdown', 'modal' ),
642 ),
643 'applyLabel' => array(
644 'type' => 'string',
645 ),
646 'cancelLabel' => array(
647 'type' => 'string',
648 ),
649 ),
650 ),
651 ),
652 ),
653 'summary' => array(
654 'oneOf' => array(
655 array( 'type' => 'string' ),
656 array(
657 'type' => 'array',
658 'items' => array(
659 'type' => 'string',
660 ),
661 ),
662 ),
663 ),
664 'editVisibility' => array(
665 'type' => 'string',
666 'enum' => array( 'always', 'on-hover' ),
667 ),
668 ),
669 ),
670 // CardLayout.
671 array(
672 'type' => 'object',
673 'properties' => array(
674 'type' => array(
675 'type' => 'string',
676 'enum' => array( 'card' ),
677 ),
678 'withHeader' => array(
679 'type' => 'boolean',
680 ),
681 'isOpened' => array(
682 'type' => 'boolean',
683 ),
684 'isCollapsible' => array(
685 'type' => 'boolean',
686 ),
687 'summary' => array(
688 'oneOf' => array(
689 array( 'type' => 'string' ),
690 array(
691 'type' => 'array',
692 'items' => array(
693 'oneOf' => array(
694 array( 'type' => 'string' ),
695 array(
696 'type' => 'object',
697 'properties' => array(
698 'id' => array(
699 'type' => 'string',
700 ),
701 'visibility' => array(
702 'type' => 'string',
703 'enum' => array( 'always', 'when-collapsed' ),
704 ),
705 ),
706 ),
707 ),
708 ),
709 ),
710 ),
711 ),
712 ),
713 ),
714 // RowLayout.
715 array(
716 'type' => 'object',
717 'properties' => array(
718 'type' => array(
719 'type' => 'string',
720 'enum' => array( 'row' ),
721 ),
722 'alignment' => array(
723 'type' => 'string',
724 'enum' => array( 'start', 'center', 'end' ),
725 ),
726 'styles' => array(
727 'type' => 'object',
728 'additionalProperties' => array(
729 'type' => 'object',
730 'properties' => array(
731 'flex' => array(
732 'type' => array( 'string', 'number' ),
733 ),
734 ),
735 ),
736 ),
737 ),
738 ),
739 // DetailsLayout.
740 array(
741 'type' => 'object',
742 'properties' => array(
743 'type' => array(
744 'type' => 'string',
745 'enum' => array( 'details' ),
746 ),
747 'summary' => array(
748 'type' => 'string',
749 ),
750 ),
751 ),
752 ),
753 );
754 }
755
756 /**
757 * Returns the schema for a form field item (string or object).
758 *
759 * @return array Schema for a form field.
760 */
761 protected function get_form_field_schema() {
762 return array(
763 'oneOf' => array(
764 array( 'type' => 'string' ),
765 array(
766 'type' => 'object',
767 'properties' => array(
768 'id' => array(
769 'type' => 'string',
770 ),
771 'label' => array(
772 'type' => 'string',
773 ),
774 'description' => array(
775 'type' => 'string',
776 ),
777 'layout' => $this->get_form_layout_schema(),
778 'children' => array(
779 'type' => 'array',
780 'items' => array(
781 'oneOf' => array(
782 array( 'type' => 'string' ),
783 // This object can have the shape of a form field itself,
784 // allowing for recursive nesting of form fields.
785 // There's no easy way to codify this recursion via the JSON Schema draft-04
786 // supported by the REST API.
787 array( 'type' => 'object' ),
788 ),
789 ),
790 ),
791 ),
792 ),
793 ),
794 );
795 }
796
797 /**
798 * Returns the schema for the form configuration object.
799 *
800 * @return array Schema properties for the form configuration.
801 */
802 protected function get_form_schema() {
803 return array(
804 'layout' => $this->get_form_layout_schema(),
805 'fields' => array(
806 'type' => 'array',
807 'items' => $this->get_form_field_schema(),
808 ),
809 );
810 }
811 }
812