class WP_REST_View_Config_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 7.1.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'view-config';
}
/**
* Registers the routes for the controller.
*
* @since 7.1.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
'kind' => array(
'description' => __( 'Entity kind.' ),
'type' => 'string',
'required' => true,
),
'name' => array(
'description' => __( 'Entity name.' ),
'type' => 'string',
'required' => true,
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read view config.
*
* @since 7.1.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$kind = $request->get_param( 'kind' );
$name = $request->get_param( 'name' );
$capability = $this->get_required_capability( $kind, $name );
if ( null === $capability ) {
return new WP_Error(
'rest_view_config_invalid_entity',
__( 'Invalid entity kind or name.' ),
array( 'status' => 404 )
);
}
if ( ! current_user_can( $capability ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to read view config.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Resolves the capability required to read the view config for an entity.
*
* Known kinds map to the capability that gates managing that entity's list:
* post types use their own `edit_posts` capability (which honors custom
* `capability_type` registrations), taxonomies use `manage_terms`, and
* root-level entities use `manage_options`. A post type or taxonomy that is
* not registered, or not exposed to the REST API, resolves to `null` so the
* request is treated as referencing an unknown entity.
*
* Any other kind falls back to `edit_posts`. This keeps entities registered
* through the `get_entity_view_config_{$kind}_{$name}` filter readable behind
* a baseline capability.
*
* @since 7.1.0
*
* @param string $kind The entity kind (e.g. `postType`).
* @param string $name The entity name (e.g. `page`).
* @return string|null Capability required to read the config, or null if the
* entity is not registered.
*/
protected function get_required_capability( $kind, $name ) {
switch ( $kind ) {
case 'postType':
$post_type = get_post_type_object( $name );
if ( $post_type && $post_type->show_in_rest ) {
return $post_type->cap->edit_posts;
}
return null;
case 'taxonomy':
$taxonomy = get_taxonomy( $name );
if ( $taxonomy && $taxonomy->show_in_rest ) {
return $taxonomy->cap->manage_terms;
}
return null;
case 'root':
return 'manage_options';
}
return 'edit_posts';
}
/**
* Returns the default view configuration for the given entity type.
*
* @since 7.1.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$kind = $request->get_param( 'kind' );
$name = $request->get_param( 'name' );
$config = wp_get_entity_view_config( $kind, $name );
$schema = $this->get_item_schema();
$response = array(
'kind' => $kind,
'name' => $name,
'version' => WP_View_Config_Data::LATEST_VERSION,
'default_view' => $this->cast_empty_objects( $config['default_view'], $schema['properties']['default_view'] ),
'default_layouts' => $this->cast_empty_objects( $config['default_layouts'], $schema['properties']['default_layouts'] ),
'view_list' => $this->cast_empty_objects( $config['view_list'], $schema['properties']['view_list'] ),
'form' => $this->cast_empty_objects( $config['form'], $schema['properties']['form'] ),
);
return rest_ensure_response( $response );
}
/**
* Recursively casts empty arrays to objects where the schema types them as
* objects.
*
* PHP cannot distinguish an empty associative array from an empty list, so
* `json_encode()` always serializes `array()` as a JSON array (`[]`). The
* REST schema, however, types several values as objects, which must encode
* as `{}`. This walks the value against its schema and casts any empty,
* object-typed array to an object. Non-empty associative arrays already
* encode as objects, so they are left as arrays and only recursed into to
* fix any nested empty objects.
*
* Union schemas (`oneOf`/`anyOf`) are handled only for the empty-array case:
* an empty value is cast to an object when any branch allows an object. Such
* values are not recursed into, which is sufficient for the form schema
* where they never contain empty nested objects.
*
* @since 7.1.0
*
* @param mixed $value The value to normalize.
* @param array $schema The schema node describing the value.
* @return mixed The normalized value, with empty object-typed arrays cast to objects.
*/
protected function cast_empty_objects( $value, $schema ) {
if ( ! is_array( $value ) || ! is_array( $schema ) ) {
return $value;
}
if ( isset( $schema['oneOf'] ) || isset( $schema['anyOf'] ) ) {
$branches = $schema['oneOf'] ?? $schema['anyOf'];
if ( array() === $value ) {
foreach ( $branches as $branch ) {
if ( is_array( $branch ) && in_array( 'object', (array) ( $branch['type'] ?? array() ), true ) ) {
return (object) array();
}
}
}
return $value;
}
$types = (array) ( $schema['type'] ?? array() );
if ( in_array( 'array', $types, true ) && isset( $schema['items'] ) ) {
foreach ( $value as $index => $item ) {
$value[ $index ] = $this->cast_empty_objects( $item, $schema['items'] );
}
return $value;
}
if ( in_array( 'object', $types, true ) ) {
if ( isset( $schema['properties'] ) ) {
foreach ( $schema['properties'] as $property => $property_schema ) {
if ( array_key_exists( $property, $value ) ) {
$value[ $property ] = $this->cast_empty_objects( $value[ $property ], $property_schema );
}
}
}
if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
foreach ( $value as $key => $item ) {
if ( isset( $schema['properties'][ $key ] ) ) {
continue;
}
$value[ $key ] = $this->cast_empty_objects( $item, $schema['additionalProperties'] );
}
}
// Empty object-typed arrays must serialize as {} to match the schema.
if ( array() === $value ) {
return (object) array();
}
}
return $value;
}
/**
* Retrieves the item's schema, conforming to JSON Schema.
*
* @since 7.1.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$view_base_properties = $this->get_view_base_schema();
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'view-config',
'type' => 'object',
'properties' => array(
'kind' => array(
'description' => __( 'Entity kind.' ),
'type' => 'string',
'readonly' => true,
),
'name' => array(
'description' => __( 'Entity name.' ),
'type' => 'string',
'readonly' => true,
),
'version' => array(
'description' => __( 'The schema version of the configuration.' ),
'type' => 'integer',
'readonly' => true,
),
'default_view' => array(
'description' => __( 'Default view configuration.' ),
'type' => 'object',
'readonly' => true,
'properties' => array_merge(
array(
'type' => array(
'type' => 'string',
),
'layout' => $this->get_combined_layout_schema(),
),
$view_base_properties
),
),
'default_layouts' => array(
'description' => __( 'Default layout configurations.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'table' => array(
'type' => 'object',
'properties' => array_merge(
$view_base_properties,
array(
'layout' => $this->get_table_layout_schema(),
)
),
),
'list' => array(
'type' => 'object',
'properties' => array_merge(
$view_base_properties,
array(
'layout' => $this->get_list_layout_schema(),
)
),
),
'grid' => array(
'type' => 'object',
'properties' => array_merge(
$view_base_properties,
array(
'layout' => $this->get_grid_layout_schema(),
)
),
),
'activity' => array(
'type' => 'object',
'properties' => array_merge(
$view_base_properties,
array(
'layout' => $this->get_list_layout_schema(),
)
),
),
'pickerGrid' => array(
'type' => 'object',
'properties' => array_merge(
$view_base_properties,
array(
'layout' => $this->get_grid_layout_schema(),
)
),
),
'pickerTable' => array(
'type' => 'object',
'properties' => array_merge(
$view_base_properties,
array(
'layout' => $this->get_table_layout_schema(),
)
),
),
),
),
'view_list' => array(
'description' => __( 'List of default views.' ),
'type' => 'array',
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'title' => array(
'type' => 'string',
),
'slug' => array(
'type' => 'string',
),
'view' => array(
'type' => 'object',
'properties' => array_merge(
array(
'type' => array(
'type' => 'string',
),
'layout' => $this->get_combined_layout_schema(),
),
$view_base_properties
),
),
),
),
),
'form' => array(
'description' => __( 'Default form configuration.' ),
'type' => 'object',
'readonly' => true,
'properties' => $this->get_form_schema(),
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Returns the schema properties shared by all view types (ViewBase), excluding 'type'.
*
* Note that `search` and `page` are not part of the schema: they are managed
* via the URL, which is their only source of truth.
*
* @since 7.1.0
*
* @return array Schema properties for the base view configuration.
*/
protected function get_view_base_schema() {
return array(
'filters' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'field' => array(
'type' => 'string',
),
'operator' => array(
'type' => 'string',
'enum' => array(
'is',
'isNot',
'isAny',
'isNone',
'isAll',
'isNotAll',
'lessThan',
'greaterThan',
'lessThanOrEqual',
'greaterThanOrEqual',
'before',
'after',
),
),
'value' => array(),
'isLocked' => array(
'type' => 'boolean',
),
),
),
),
'sort' => array(
'type' => 'object',
'properties' => array(
'field' => array(
'type' => 'string',
),
'direction' => array(
'type' => 'string',
'enum' => array( 'asc', 'desc' ),
),
),
),
'perPage' => array(
'type' => 'integer',
),
'fields' => array(
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
'titleField' => array(
'type' => 'string',
),
'mediaField' => array(
'type' => 'string',
),
'descriptionField' => array(
'type' => 'string',
),
'showTitle' => array(
'type' => 'boolean',
),
'showMedia' => array(
'type' => 'boolean',
),
'showDescription' => array(
'type' => 'boolean',
),
'showLevels' => array(
'type' => 'boolean',
),
'groupBy' => array(
'type' => 'object',
'properties' => array(
'field' => array(
'type' => 'string',
),
'direction' => array(
'type' => 'string',
'enum' => array( 'asc', 'desc' ),
),
'showLabel' => array(
'type' => 'boolean',
'default' => true,
),
),
),
'infiniteScrollEnabled' => array(
'type' => 'boolean',
),
);
}
/**
* Returns the schema for the ColumnStyle type.
*
* @since 7.1.0
*
* @return array Schema for a column style object.
*/
protected function get_column_style_schema() {
return array(
'type' => 'object',
'properties' => array(
'width' => array(
'type' => array( 'string', 'number' ),
),
'maxWidth' => array(
'type' => array( 'string', 'number' ),
),
'minWidth' => array(
'type' => array( 'string', 'number' ),
),
'align' => array(
'type' => 'string',
'enum' => array( 'start', 'center', 'end' ),
),
),
);
}
/**
* Returns the layout schema for table-type views (ViewTable, ViewPickerTable).
*
* @since 7.1.0
*
* @return array Schema for a table layout object.
*/
protected function get_table_layout_schema() {
return array(
'type' => 'object',
'properties' => array(
'styles' => array(
'type' => 'object',
'additionalProperties' => $this->get_column_style_schema(),
),
'density' => array(
'type' => 'string',
'enum' => array( 'compact', 'balanced', 'comfortable' ),
),
'enableMoving' => array(
'type' => 'boolean',
),
),
);
}
/**
* Returns the layout schema for list-type views (ViewList, ViewActivity).
*
* @since 7.1.0
*
* @return array Schema for a list layout object.
*/
protected function get_list_layout_schema() {
return array(
'type' => 'object',
'properties' => array(
'density' => array(
'type' => 'string',
'enum' => array( 'compact', 'balanced', 'comfortable' ),
),
),
);
}
/**
* Returns a combined layout schema that accepts properties from all view types.
*
* This is useful for contexts where the view type is not known ahead of time
* (e.g. the `view` override in a view list item), so all possible layout
* properties must be accepted.
*
* @since 7.1.0
*
* @return array Schema for a combined layout object.
*/
protected function get_combined_layout_schema() {
return array(
'type' => 'object',
'properties' => array_merge(
$this->get_table_layout_schema()['properties'],
$this->get_grid_layout_schema()['properties'],
$this->get_list_layout_schema()['properties']
),
);
}
/**
* Returns the layout schema for grid-type views (ViewGrid, ViewPickerGrid).
*
* @since 7.1.0
*
* @return array Schema for a grid layout object.
*/
protected function get_grid_layout_schema() {
return array(
'type' => 'object',
'properties' => array(
'badgeFields' => array(
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
'previewSize' => array(
'type' => 'number',
),
'density' => array(
'type' => 'string',
'enum' => array( 'compact', 'balanced', 'comfortable' ),
),
),
);
}
/**
* Returns the schema for a form layout object as a discriminated union.
*
* Each variant is discriminated by a single-value enum on its `type` property,
* matching the TypeScript Layout union in dataviews/src/types/dataform.ts.
*
* @since 7.1.0
*
* @return array Schema for a form layout object.
*/
protected function get_form_layout_schema() {
return array(
'oneOf' => array(
// RegularLayout.
array(
'type' => 'object',
'properties' => array(
'type' => array(
'type' => 'string',
'enum' => array( 'regular' ),
),
'labelPosition' => array(
'type' => 'string',
'enum' => array( 'top', 'side', 'none' ),
),
),
),
// PanelLayout.
array(
'type' => 'object',
'properties' => array(
'type' => array(
'type' => 'string',
'enum' => array( 'panel' ),
),
'labelPosition' => array(
'type' => 'string',
'enum' => array( 'top', 'side', 'none' ),
),
'openAs' => array(
'oneOf' => array(
array(
'type' => 'string',
'enum' => array( 'dropdown', 'modal' ),
),
array(
'type' => 'object',
'properties' => array(
'type' => array(
'type' => 'string',
'enum' => array( 'dropdown', 'modal' ),
),
'applyLabel' => array(
'type' => 'string',
),
'cancelLabel' => array(
'type' => 'string',
),
),
),
),
),
'summary' => array(
'oneOf' => array(
array( 'type' => 'string' ),
array(
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
),
),
'editVisibility' => array(
'type' => 'string',
'enum' => array( 'always', 'on-hover' ),
),
),
),
// CardLayout.
array(
'type' => 'object',
'properties' => array(
'type' => array(
'type' => 'string',
'enum' => array( 'card' ),
),
'withHeader' => array(
'type' => 'boolean',
),
'isOpened' => array(
'type' => 'boolean',
),
'isCollapsible' => array(
'type' => 'boolean',
),
'summary' => array(
'oneOf' => array(
array( 'type' => 'string' ),
array(
'type' => 'array',
'items' => array(
'oneOf' => array(
array( 'type' => 'string' ),
array(
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'string',
),
'visibility' => array(
'type' => 'string',
'enum' => array( 'always', 'when-collapsed' ),
),
),
),
),
),
),
),
),
),
),
// RowLayout.
array(
'type' => 'object',
'properties' => array(
'type' => array(
'type' => 'string',
'enum' => array( 'row' ),
),
'alignment' => array(
'type' => 'string',
'enum' => array( 'start', 'center', 'end' ),
),
'styles' => array(
'type' => 'object',
'additionalProperties' => array(
'type' => 'object',
'properties' => array(
'flex' => array(
'type' => array( 'string', 'number' ),
),
),
),
),
),
),
// DetailsLayout.
array(
'type' => 'object',
'properties' => array(
'type' => array(
'type' => 'string',
'enum' => array( 'details' ),
),
'summary' => array(
'type' => 'string',
),
),
),
),
);
}
/**
* Returns the schema for a form field item (string or object).
*
* @since 7.1.0
*
* @return array Schema for a form field.
*/
protected function get_form_field_schema() {
return array(
'oneOf' => array(
array( 'type' => 'string' ),
array(
'type' => 'object',
'properties' => array(
'id' => array(
'type' => 'string',
),
'label' => array(
'type' => 'string',
),
'description' => array(
'type' => 'string',
),
'layout' => $this->get_form_layout_schema(),
'children' => array(
'type' => 'array',
'items' => array(
'oneOf' => array(
array( 'type' => 'string' ),
// This object can have the shape of a form field itself,
// allowing for recursive nesting of form fields.
// There's no easy way to codify this recursion via the JSON Schema draft-04
// supported by the REST API.
array( 'type' => 'object' ),
),
),
),
),
),
),
);
}
/**
* Returns the schema for the form configuration object.
*
* @since 7.1.0
*
* @return array Schema properties for the form configuration.
*/
protected function get_form_schema() {
return array(
'layout' => $this->get_form_layout_schema(),
'fields' => array(
'type' => 'array',
'items' => $this->get_form_field_schema(),
),
);
}
}
View all references View on Trac View on GitHub
User Contributed Notes
You must log in before being able to contribute a note or feedback.