policy = $policy ?? new CommandPolicy(); } /** * Run the shared validate/execute pipeline for one wp.schema.get command. * * @param array $input Command input document. * @param string $phase Either `validate` or `execute`. * @return array Internal result: `['data' => array]` or `['error' => array]`. */ public function run( array $input, string $phase ): array { $resource = isset( $input['resource'] ) && is_scalar( $input['resource'] ) ? (string) $input['resource'] : ''; if ( 'posts' !== $resource ) { if ( 'post_types' === $resource ) { return $this->run_post_types_schema( $input, $phase ); } if ( in_array( $resource, $this->core_content_resources(), true ) ) { return $this->run_core_content_schema( $resource, $input, $phase ); } if ( 'menus' === $resource ) { return $this->run_menus_schema( $input, $phase ); } if ( in_array( $resource, $this->theme_identity_resources(), true ) ) { return $this->run_theme_identity_schema( $resource, $input, $phase ); } if ( in_array( $resource, $this->specials_maintenance_resources(), true ) ) { return $this->run_specials_maintenance_schema( $resource, $input, $phase ); } return $this->run_registry_schema( $resource, $input, $phase ); } $target = isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(); if ( isset( $target['post_type'] ) && ! is_scalar( $target['post_type'] ) ) { return $this->error( 'invalid_target', 'target.post_type must be a scalar value.', array( 'input', 'target', 'post_type' ) ); } $post_type = isset( $target['post_type'] ) ? sanitize_key( (string) $target['post_type'] ) : 'post'; if ( '' === $post_type || ! in_array( $post_type, $this->registered_post_types(), true ) ) { return $this->error( 'invalid_target', 'target.post_type is not registered on this site.', array( 'input', 'target', 'post_type' ) ); } $paths_under = $input['paths_under'] ?? null; if ( null !== $paths_under && ! $this->is_path( $paths_under ) ) { return $this->error( 'invalid_command', 'paths_under must be an array-segment path.', array( 'input', 'paths_under' ) ); } $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $all_paths = $this->paths_for_posts( $post_type ); $paths = $all_paths; if ( is_array( $paths_under ) ) { $paths = array_values( array_filter( $paths, static fn ( array $entry ): bool => self::path_starts_with( $entry['path'], $paths_under ) ) ); } $data = array( 'resource' => 'posts', 'target' => array( 'post_type' => $post_type ), ); if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = array( 'set', 'remove', 'insert' ); } if ( in_array( 'actions', $sections, true ) ) { $data['actions'] = array( 'query', 'get', 'create', 'update', 'delete', 'clone' ); } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = array( array( 'post_type' ), array( 'title', 'raw' ) ); } if ( in_array( 'providers', $sections, true ) ) { $providers = $this->providers_for( $all_paths ); // seo.* writes are provider-backed; only advertise the provider when a // supported SEO plugin is actually active. if ( '' !== SeoWriteCapability::active_supported_plugin() ) { $providers[] = 'seo'; } $data['providers'] = $providers; } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = $this->declared_enums( $post_type ); } return array( 'data' => $data ); } private function run_post_types_schema( array $input, string $phase ): array { $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } $paths_under = $input['paths_under'] ?? null; if ( null !== $paths_under && ! $this->is_path( $paths_under ) ) { return $this->error( 'invalid_command', 'paths_under must be an array-segment path.', array( 'input', 'paths_under' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $providers = $this->post_types_provider_statuses(); $all_paths = $this->post_types_paths( $providers ); $paths = $all_paths; if ( is_array( $paths_under ) ) { $paths = array_values( array_filter( $paths, static fn ( array $entry ): bool => self::path_starts_with( $entry['path'], $paths_under ) ) ); } $data = array( 'resource' => 'post_types', 'target' => isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(), ); if ( isset( $data['target']['name'] ) && is_scalar( $data['target']['name'] ) ) { $target_provider = PostTypesWriteCapability::provider_owner_for_post_type( sanitize_key( (string) $data['target']['name'] ) ); if ( '' !== $target_provider ) { $data['target_provider'] = $target_provider; } } if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = $this->post_types_provider_has_action( $providers, 'update' ) ? array( 'set' ) : array(); } if ( in_array( 'actions', $sections, true ) ) { $actions = array( 'query', 'get' ); if ( $this->post_types_provider_has_action( $providers, 'create' ) ) { $actions[] = 'create'; } if ( $this->post_types_provider_has_action( $providers, 'update' ) ) { $actions[] = 'update'; } $data['actions'] = $actions; } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = array( array( 'post_type' ), array( 'label' ), array( 'plural_label' ) ); } if ( in_array( 'providers', $sections, true ) ) { $data['providers'] = $providers; } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = array( 'provider' => array_values( array_map( static fn ( array $provider ): string => (string) $provider['id'], $providers ) ), 'field_type' => $this->post_types_supported_field_types( $providers ), 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments', 'revisions', 'custom-fields', 'page-attributes' ), 'taxonomies' => $this->registered_taxonomies_for_schema(), ); } return array( 'data' => $data ); } private function run_menus_schema( array $input, string $phase ): array { $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $paths = $this->menus_paths(); $data = array( 'resource' => 'menus', 'target' => isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(), ); if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = array( 'set', 'remove', 'insert' ); } if ( in_array( 'actions', $sections, true ) ) { $data['actions'] = array( 'query', 'get', 'create', 'update', 'delete' ); } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = array( array( 'name' ) ); } if ( in_array( 'providers', $sections, true ) ) { $data['providers'] = $this->providers_for( $paths ); } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = array( 'item_type' => array( 'custom', 'post_type', 'taxonomy' ) ); } return array( 'data' => $data ); } private function menus_paths(): array { $can_read = current_user_can( 'read' ); $can_write = current_user_can( 'edit_theme_options' ); return array( $this->menus_path( array( 'id' ), 'integer', false, $can_read, true, true ), $this->menus_path( array( 'name' ), 'string', $can_write, $can_read, true, true ), $this->menus_path( array( 'slug' ), 'string', $can_write, $can_read, true, true ), $this->menus_path( array( 'locations' ), 'array', $can_write, $can_read, true, false ), $this->menus_path( array( 'item_count' ), 'integer', false, $can_read, true, true ), $this->menus_path( array( 'items' ), 'array', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*' ), 'object', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'id' ), 'integer', false, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'label' ), 'string', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'type' ), 'string', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'url' ), 'string', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'object' ), 'object', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'parent' ), 'integer', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'order' ), 'integer', false, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'target' ), 'string', $can_write, $can_read, false, false ), $this->menus_path( array( 'items', '*', 'classes' ), 'array', $can_write, $can_read, false, false ), ); } private function menus_path( array $path, string $type, bool $write, bool $read, bool $query, bool $order ): array { $entry = array( 'path' => $path, 'type' => $type, 'provider' => 'core', 'read' => $read, 'write' => $write, 'query' => $query, 'order' => $order, ); if ( $query ) { $entry['operators'] = MenusCapability::QUERY_OPS; } if ( $this->path_has_prefix( $path, array( 'items' ) ) ) { $entry['read_scope'] = 'get'; } return $entry; } /** * Return effective schema for core content families. * * @param string $resource_name Resource name. * @param array $input Command input. * @param string $phase validate or execute. */ private function run_core_content_schema( string $resource_name, array $input, string $phase ): array { $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $paths = $this->core_content_paths( $resource_name ); $actions = $this->core_content_actions( $resource_name ); $data = array( 'resource' => $resource_name, 'target' => isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(), ); if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = in_array( 'update', $actions, true ) ? array( 'set', 'remove', 'insert' ) : array(); } if ( in_array( 'actions', $sections, true ) ) { $data['actions'] = $actions; } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = $this->core_content_create_required( $resource_name ); } if ( in_array( 'providers', $sections, true ) ) { $data['providers'] = $this->providers_for( $paths ); } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = $this->core_content_enums( $resource_name ); } return array( 'data' => $data ); } /** * Return core content resource names. * * @return array */ private function core_content_resources(): array { $resources = array(); foreach ( CoreContentCapability::CAPABILITIES as $metadata ) { $resources[ (string) $metadata['resource'] ] = (string) $metadata['resource']; } return array_values( $resources ); } /** * Return supported actions for one core content resource. * * @return array */ private function core_content_actions( string $resource_name ): array { $actions = array(); foreach ( CoreContentCapability::CAPABILITIES as $metadata ) { if ( $resource_name === (string) $metadata['resource'] ) { $actions[] = (string) $metadata['action']; } } return $actions; } /** * Build effective path table entries for core content families. * * @return array> */ private function core_content_paths( string $resource_name ): array { $read_only = in_array( $resource_name, array( 'users', 'revisions' ), true ); $paths = match ( $resource_name ) { 'terms' => array( $this->core_content_path( 'terms', array( 'id' ), 'integer', false, true, true ), $this->core_content_path( 'terms', array( 'taxonomy' ), 'string', false, true, true ), $this->core_content_path( 'terms', array( 'name' ), 'string', true, true, true ), $this->core_content_path( 'terms', array( 'slug' ), 'string', true, true, true ), $this->core_content_path( 'terms', array( 'parent' ), 'integer', true, true, true ), $this->core_content_path( 'terms', array( 'count' ), 'integer', false, true, true ), $this->core_content_path( 'terms', array( 'meta', '*' ), 'any', true, true, false, 'meta' ), ), 'media' => array( $this->core_content_path( 'media', array( 'id' ), 'integer', false, true, true ), $this->core_content_path( 'media', array( 'title', 'raw' ), 'string', true, true, false ), $this->core_content_path( 'media', array( 'caption', 'raw' ), 'string', true, true, false ), $this->core_content_path( 'media', array( 'description', 'raw' ), 'string', true, true, false ), $this->core_content_path( 'media', array( 'mime_type' ), 'string', false, true, true ), $this->core_content_path( 'media', array( 'url' ), 'string', false, true, false, 'core', true ), $this->core_content_path( 'media', array( 'source', 'url' ), 'string', false, false, false, 'core', true ), $this->core_content_path( 'media', array( 'meta', '*' ), 'any', true, true, false, 'meta' ), ), 'comments' => array( $this->core_content_path( 'comments', array( 'id' ), 'integer', false, true, true ), $this->core_content_path( 'comments', array( 'post_id' ), 'integer', false, true, true, 'core', true ), $this->core_content_path( 'comments', array( 'status' ), 'string', true, true, true ), $this->core_content_path( 'comments', array( 'author', 'name' ), 'string', false, true, false, 'core', true ), $this->core_content_path( 'comments', array( 'author', 'url' ), 'string', false, true, false, 'core', true ), $this->core_content_path( 'comments', array( 'content', 'raw' ), 'string', true, true, false ), $this->core_content_path( 'comments', array( 'date_gmt' ), 'string', false, true, true ), $this->core_content_path( 'comments', array( 'meta', '*' ), 'any', true, true, false, 'meta' ), ), 'options' => array( $this->core_content_path( 'options', array( 'identity', 'name' ), 'string', false, true, true ), $this->core_content_path( 'options', array( 'value_summary' ), 'object', false, true, false ), $this->core_content_path( 'options', array( 'value' ), 'any', true, true, false ), $this->core_content_path( 'options', array( 'autoload' ), 'boolean', true, true, true ), ), 'users' => array( $this->core_content_path( 'users', array( 'id' ), 'integer', false, true, true ), $this->core_content_path( 'users', array( 'login' ), 'string', false, true, true ), $this->core_content_path( 'users', array( 'name' ), 'string', false, true, true ), $this->core_content_path( 'users', array( 'roles' ), 'array', false, true, false ), $this->core_content_path( 'users', array( 'meta', '*' ), 'any', false, true, false, 'meta' ), ), 'revisions' => array( $this->core_content_path( 'revisions', array( 'id' ), 'integer', false, true, true ), $this->core_content_path( 'revisions', array( 'post_id' ), 'integer', false, true, true ), $this->core_content_path( 'revisions', array( 'title', 'raw' ), 'string', false, true, false ), $this->core_content_path( 'revisions', array( 'content', 'raw' ), 'string', false, true, false ), $this->core_content_path( 'revisions', array( 'excerpt', 'raw' ), 'string', false, true, false ), $this->core_content_path( 'revisions', array( 'modified_gmt' ), 'string', false, true, true ), $this->core_content_path( 'revisions', array( 'author', 'id' ), 'integer', false, true, false ), ), default => array(), }; if ( $read_only ) { foreach ( $paths as &$path ) { $path['write'] = false; } unset( $path ); } return $paths; } private function core_content_path( string $resource_name, array $path, string $type, bool $write, bool $query, bool $order, string $provider = 'core', bool $create = false ): array { $entry = array( 'path' => $path, 'type' => $type, 'provider' => $provider, 'read' => true, 'write' => $write, 'query' => $query, 'order' => $order, ); if ( $query ) { $entry['operators'] = CoreContentCapability::QUERY_OPS; } if ( $create ) { $entry['create'] = true; } return $entry; } private function core_content_create_required( string $resource_name ): array { return match ( $resource_name ) { 'terms' => array( array( 'taxonomy' ), array( 'name' ) ), 'comments' => array( array( 'post_id' ), array( 'content', 'raw' ) ), 'media' => array( array( 'source', 'url' ) ), default => array(), }; } private function core_content_enums( string $resource_name ): array { return 'comments' === $resource_name ? array( 'status' => array( 'values' => array( 'hold', 'approve', 'spam', 'trash' ), 'aliases' => array( 'pending' => 'hold', 'approved' => 'approve', ), ), ) : array(); } /** * Return effective schema for theme/editor families. * * @param string $resource_name Resource name. * @param array $input Command input. * @param string $phase validate or execute. */ private function run_theme_identity_schema( string $resource_name, array $input, string $phase ): array { $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $paths = $this->theme_identity_paths( $resource_name ); $actions = $this->theme_identity_actions( $resource_name ); $data = array( 'resource' => $resource_name, 'target' => isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(), ); if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = in_array( 'update', $actions, true ) ? array( 'set', 'remove', 'insert' ) : array(); } if ( in_array( 'actions', $sections, true ) ) { $data['actions'] = $actions; } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = array(); } if ( in_array( 'providers', $sections, true ) ) { $data['providers'] = $this->providers_for( $paths ); } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = 'global_styles' === $resource_name ? array() : array( 'source' => array( 'theme', 'custom' ) ); } return array( 'data' => $data ); } /** * Return resource names served by ThemeIdentityCapability. * * @return array */ private function theme_identity_resources(): array { $resources = array(); foreach ( ThemeIdentityCapability::CAPABILITIES as $metadata ) { $resources[ (string) $metadata['resource'] ] = (string) $metadata['resource']; } return array_values( $resources ); } /** * Return supported actions for a theme-identity resource. * * @return array */ private function theme_identity_actions( string $resource_name ): array { $actions = array(); foreach ( ThemeIdentityCapability::CAPABILITIES as $metadata ) { if ( $resource_name === (string) $metadata['resource'] ) { $actions[] = (string) $metadata['action']; } } return $actions; } /** * Return effective schema for special and maintenance families. * * @param string $resource_name Resource name. * @param array $input Command input. * @param string $phase validate or execute. */ private function run_specials_maintenance_schema( string $resource_name, array $input, string $phase ): array { $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $paths = $this->specials_maintenance_paths( $resource_name ); $actions = $this->specials_maintenance_actions( $resource_name ); $data = array( 'resource' => $resource_name, 'target' => isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(), ); if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = $this->specials_maintenance_change_ops( $resource_name ); } if ( in_array( 'actions', $sections, true ) ) { $data['actions'] = $actions; } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = $this->specials_maintenance_create_required( $resource_name ); } if ( in_array( 'providers', $sections, true ) ) { $data['providers'] = $this->providers_for( $paths ); } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = 'files' === $resource_name ? array( 'scope' => RegistryReadCapability::FILE_SCOPES ) : array(); } return array( 'data' => $data ); } /** * Return resource names served by SpecialsMaintenanceCapability. * * @return array */ private function specials_maintenance_resources(): array { $resources = array(); foreach ( SpecialsMaintenanceCapability::CAPABILITIES as $metadata ) { $resources[ (string) $metadata['resource'] ] = (string) $metadata['resource']; } return array_values( $resources ); } /** * Return supported actions for a special/maintenance resource. * * @return array */ private function specials_maintenance_actions( string $resource_name ): array { $actions = array(); if ( in_array( $resource_name, $this->registry_resources(), true ) ) { foreach ( $this->registry_actions( $resource_name ) as $action ) { $actions[ $action ] = $action; } } foreach ( SpecialsMaintenanceCapability::CAPABILITIES as $metadata ) { if ( $resource_name === (string) $metadata['resource'] ) { $actions[ (string) $metadata['action'] ] = (string) $metadata['action']; } } return array_values( $actions ); } /** * Build effective paths for special/maintenance families. * * @return array> */ private function specials_maintenance_paths( string $resource_name ): array { return match ( $resource_name ) { 'http' => array( $this->specials_maintenance_path( 'http', array( 'url' ), 'string', true, true, false ), $this->specials_maintenance_path( 'http', array( 'method' ), 'string', true, true, false ), $this->specials_maintenance_path( 'http', array( 'headers' ), 'object', true, true, false ), $this->specials_maintenance_path( 'http', array( 'timeout' ), 'number', true, true, false ), $this->specials_maintenance_path( 'http', array( 'response' ), 'object', false, true, false ), $this->specials_maintenance_path( 'http', array( 'response', 'status' ), 'integer', false, true, false ), $this->specials_maintenance_path( 'http', array( 'response', 'status_text' ), 'string', false, true, false ), $this->specials_maintenance_path( 'http', array( 'response', 'headers' ), 'object', false, true, false ), $this->specials_maintenance_path( 'http', array( 'response', 'redirects' ), 'array', false, true, false ), $this->specials_maintenance_path( 'http', array( 'response', 'body' ), 'string', false, true, false ), ), 'performance' => array( $this->specials_maintenance_path( 'performance', array( 'url' ), 'string', false, true, false ), $this->specials_maintenance_path( 'performance', array( 'metrics' ), 'object', false, true, false ), ), 'cron_events' => array( $this->specials_maintenance_path( 'cron_events', array( 'hook' ), 'string', true, true, true ), $this->specials_maintenance_path( 'cron_events', array( 'timestamp' ), 'integer', true, true, true ), $this->specials_maintenance_path( 'cron_events', array( 'schedule' ), 'string', true, true, true ), // args is readable/writable but not queryable: the scalar predicate // matcher has no array-value semantics to offer for it. $this->specials_maintenance_path( 'cron_events', array( 'args' ), 'array', true, false, false ), ), 'files' => array( $this->specials_maintenance_path( 'files', array( 'identity', 'scope' ), 'string', false, true, true ), $this->specials_maintenance_path( 'files', array( 'identity', 'path' ), 'string', false, true, true ), $this->specials_maintenance_path( 'files', array( 'metadata' ), 'object', false, true, true ), $this->specials_maintenance_path( 'files', array( 'snippet' ), 'string', false, true, false ), $this->specials_maintenance_path( 'files', array( 'content' ), 'string', true, false, false, 'get' ), ), default => array( $this->specials_maintenance_default_path( $resource_name ) ), }; } private function specials_maintenance_change_ops( string $resource_name ): array { return 'files' === $resource_name ? array( 'set', 'remove', 'insert' ) : array(); } private function specials_maintenance_default_path( string $resource_name ): array { return $this->specials_maintenance_path( $resource_name, array( 'summary' ), 'object', false, true, false ); } private function specials_maintenance_path( string $resource_name, array $path, string $type, bool $write, bool $query, bool $order, ?string $read_scope = null ): array { $entry = array( 'path' => $path, 'type' => $type, 'provider' => $resource_name, 'read' => true, 'write' => $write, 'query' => $query, 'order' => $order, ); if ( $query ) { $entry['operators'] = array( 'eq', 'neq', 'in', 'not_in', 'contains', 'exists' ); } if ( null !== $read_scope ) { $entry['read_scope'] = $read_scope; } return $entry; } private function specials_maintenance_create_required( string $resource_name ): array { return match ( $resource_name ) { 'cron_events' => array( array( 'hook' ), array( 'timestamp' ) ), 'files' => array( array( 'identity', 'scope' ), array( 'identity', 'path' ), array( 'content' ) ), default => array(), }; } /** * Build effective path table entries for theme-identity families. * * @return array> */ private function theme_identity_paths( string $resource_name ): array { if ( 'global_styles' === $resource_name ) { return array( $this->theme_identity_path( $resource_name, array( 'version' ), 'integer', true, false, false ), $this->theme_identity_path( $resource_name, array( 'settings' ), 'object', true, false, false, 'theme_json' ), $this->theme_identity_path( $resource_name, array( 'settings', '*' ), 'any', true, false, false, 'theme_json' ), $this->theme_identity_path( $resource_name, array( 'styles' ), 'object', true, false, false, 'theme_json' ), $this->theme_identity_path( $resource_name, array( 'styles', '*' ), 'any', true, false, false, 'theme_json' ), ); } return array( $this->theme_identity_path( $resource_name, array( 'identity', 'id' ), 'string', false, true, true ), $this->theme_identity_path( $resource_name, array( 'identity', 'theme' ), 'string', false, true, true ), $this->theme_identity_path( $resource_name, array( 'identity', 'slug' ), 'string', false, true, true ), $this->theme_identity_path( $resource_name, array( 'title', 'raw' ), 'string', true, true, false ), $this->theme_identity_path( $resource_name, array( 'description', 'raw' ), 'string', true, true, false ), $this->theme_identity_path( $resource_name, array( 'area' ), 'string', true, true, true ), $this->theme_identity_path( $resource_name, array( 'content', 'raw' ), 'string', true, false, false ), $this->theme_identity_path( $resource_name, array( 'blocks' ), 'array', true, false, false ), $this->theme_identity_path( $resource_name, array( 'blocks', '*', 'attrs', '*' ), 'any', true, false, false ), $this->theme_identity_path( $resource_name, array( 'status', 'customized' ), 'boolean', false, true, false ), $this->theme_identity_path( $resource_name, array( 'status', 'source' ), 'string', false, true, true ), ); } private function theme_identity_path( string $resource_name, array $path, string $type, bool $write, bool $query, bool $order, string $provider = 'core' ): array { $entry = array( 'path' => $path, 'type' => $type, 'provider' => $provider, 'read' => true, 'write' => $write, 'query' => $query, 'order' => $order, ); if ( $query ) { $entry['operators'] = array( 'eq', 'neq', 'in', 'not_in', 'contains', 'exists' ); } if ( $this->path_has_prefix( $path, array( 'blocks' ) ) ) { $entry['read_scope'] = 'get'; } return $entry; } /** * Return effective schema for read-mostly registry/singleton families. * * @param string $resource_name Resource name. * @param array $input Command input. * @param string $phase validate or execute. */ private function run_registry_schema( string $resource_name, array $input, string $phase ): array { if ( ! in_array( $resource_name, $this->registry_resources(), true ) ) { return $this->error( 'invalid_target', 'wp.schema.get does not support the requested resource.', array( 'input', 'resource' ) ); } $include = $input['include'] ?? null; if ( null !== $include && ! $this->is_include_list( $include ) ) { return $this->error( 'invalid_command', 'include must be a list of schema sections.', array( 'input', 'include' ) ); } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $sections = null === $include ? self::DEFAULT_SECTIONS : array_values( $include ); $paths = $this->registry_paths( $resource_name ); $data = array( 'resource' => $resource_name, 'target' => isset( $input['target'] ) && is_array( $input['target'] ) ? $input['target'] : array(), ); if ( in_array( 'paths', $sections, true ) ) { $data['paths'] = $paths; } if ( in_array( 'operations', $sections, true ) ) { $data['change_ops'] = array(); } if ( in_array( 'actions', $sections, true ) ) { $data['actions'] = $this->registry_actions( $resource_name ); } if ( in_array( 'create_required', $sections, true ) ) { $data['create_required'] = array(); } if ( in_array( 'providers', $sections, true ) ) { $data['providers'] = $this->providers_for( $paths ); } if ( in_array( 'enums', $sections, true ) ) { $data['enums'] = 'files' === $resource_name ? array( 'scope' => RegistryReadCapability::FILE_SCOPES ) : array(); } return array( 'data' => $data ); } /** * Return resource names served by RegistryReadCapability. * * @return array */ private function registry_resources(): array { $resources = array(); foreach ( RegistryReadCapability::CAPABILITIES as $metadata ) { $resources[ (string) $metadata['resource'] ] = (string) $metadata['resource']; } return array_values( $resources ); } /** * Return supported actions for a registry resource. * * @param string $resource_name Resource name. * @return array */ private function registry_actions( string $resource_name ): array { $actions = array(); foreach ( RegistryReadCapability::CAPABILITIES as $metadata ) { if ( $resource_name === (string) $metadata['resource'] ) { $actions[ (string) $metadata['action'] ] = (string) $metadata['action']; } } return array_values( $actions ); } /** * Build a small effective path table for read-mostly families. * * @param string $resource_name Resource name. * @return array> */ private function registry_paths( string $resource_name ): array { if ( in_array( $resource_name, array( 'runtime', 'server', 'site_health', 'core' ), true ) ) { return $this->singleton_registry_paths( $resource_name ); } $paths = array( $this->registry_path( array( 'identity' ), 'object', $resource_name, true, true ), ); // Only resources whose documents really carry a summary root advertise one. if ( ! in_array( $resource_name, array( 'plugins', 'themes', 'files', 'assets' ), true ) ) { $paths[] = $this->registry_path( array( 'summary' ), 'object', $resource_name, true, true ); } if ( in_array( $resource_name, array( 'post_types', 'taxonomies' ), true ) ) { $paths[] = $this->registry_path( array( 'identity', 'name' ), 'string', $resource_name, true, true ); $paths[] = $this->registry_path( array( 'summary', 'label' ), 'string', $resource_name, true, true ); $paths[] = $this->registry_path( array( 'summary', 'show_in_rest' ), 'boolean', $resource_name, true, true ); $paths[] = $this->registry_path( array( 'summary', 'rest_base' ), 'string', $resource_name, true, true ); $paths[] = $this->registry_path( array( 'hierarchical' ), 'boolean', $resource_name, true, true ); $paths[] = $this->registry_path( array( 'labels', 'name' ), 'string', $resource_name, true, true ); $paths[] = $this->registry_path( array( 'labels', 'singular_name' ), 'string', $resource_name, true, true ); } if ( 'post_types' === $resource_name ) { $paths[] = $this->registry_path( array( 'identity', 'provider' ), 'string', 'post_types', true, true ); $paths[] = $this->registry_path( array( 'summary', 'provider' ), 'string', 'post_types', true, true ); $paths[] = $this->registry_path( array( 'supports' ), 'array', 'post_types', true, false ); $paths[] = $this->registry_path( array( 'taxonomies' ), 'array', 'post_types', true, false ); $paths[] = $this->registry_path( array( 'rest', 'show_in_rest' ), 'boolean', 'post_types', true, true ); $paths[] = $this->registry_path( array( 'rest', 'rest_base' ), 'string', 'post_types', true, true ); } if ( 'taxonomies' === $resource_name ) { $paths[] = $this->registry_path( array( 'summary', 'object_types' ), 'array', 'taxonomies', true, false ); $paths[] = $this->registry_path( array( 'object_types' ), 'array', 'taxonomies', true, false ); $paths[] = $this->registry_path( array( 'rest', 'show_in_rest' ), 'boolean', 'taxonomies', true, true ); $paths[] = $this->registry_path( array( 'rest', 'rest_base' ), 'string', 'taxonomies', true, true ); } if ( 'plugins' === $resource_name ) { $paths[] = $this->registry_path( array( 'identity', 'plugin' ), 'string', 'plugins', true, true ); $paths[] = $this->registry_path( array( 'identity', 'name' ), 'string', 'plugins', true, true ); $paths[] = $this->registry_path( array( 'identity', 'version' ), 'string', 'plugins', true, true ); $paths[] = $this->registry_path( array( 'status', 'active' ), 'boolean', 'plugins', true, true ); $paths[] = $this->registry_path( array( 'updates', 'available' ), 'boolean', 'plugins', true, false ); } if ( 'themes' === $resource_name ) { $paths[] = $this->registry_path( array( 'identity', 'theme' ), 'string', 'themes', true, true ); $paths[] = $this->registry_path( array( 'identity', 'name' ), 'string', 'themes', true, true ); $paths[] = $this->registry_path( array( 'identity', 'version' ), 'string', 'themes', true, true ); $paths[] = $this->registry_path( array( 'status', 'active' ), 'boolean', 'themes', true, true ); $paths[] = $this->registry_path( array( 'updates', 'available' ), 'boolean', 'themes', true, false ); } if ( 'block_patterns' === $resource_name ) { $paths[] = $this->registry_path( array( 'content', 'raw' ), 'string', 'block_patterns', true, false ); } if ( 'files' === $resource_name ) { $paths[] = $this->registry_path( array( 'metadata' ), 'object', 'files', true, true ); $paths[] = $this->registry_path( array( 'snippet' ), 'string', 'files', true, false ); } if ( 'assets' === $resource_name ) { $paths[] = $this->registry_path( array( 'identity', 'handle' ), 'string', 'assets', true, true ); $paths[] = $this->registry_path( array( 'identity', 'type' ), 'string', 'assets', true, true ); $paths[] = $this->registry_path( array( 'identity', 'enqueued' ), 'boolean', 'assets', true, true ); $paths[] = $this->registry_path( array( 'source' ), 'object', 'assets', true, true ); $paths[] = $this->registry_path( array( 'source', 'src' ), 'string', 'assets', true, true ); $paths[] = $this->registry_path( array( 'source', 'version' ), 'string', 'assets', true, true ); $paths[] = $this->registry_path( array( 'source', 'deps' ), 'array', 'assets', true, false ); $paths[] = $this->registry_path( array( 'owner' ), 'object', 'assets', true, true ); $paths[] = $this->registry_path( array( 'loading' ), 'object', 'assets', true, false ); $paths[] = $this->registry_path( array( 'localized' ), 'object', 'assets', true, false ); $paths[] = $this->registry_path( array( 'duplicates' ), 'object', 'assets', true, false ); } return $paths; } private function singleton_registry_paths( string $resource_name ): array { return match ( $resource_name ) { 'runtime' => array( $this->registry_path( array( 'wordpress' ), 'object', 'runtime', false, false ), $this->registry_path( array( 'wordpress', 'version' ), 'string', 'runtime', false, false ), $this->registry_path( array( 'php' ), 'object', 'runtime', false, false ), $this->registry_path( array( 'php', 'version' ), 'string', 'runtime', false, false ), $this->registry_path( array( 'debug' ), 'object', 'runtime', false, false ), $this->registry_path( array( 'debug', 'wp_debug' ), 'boolean', 'runtime', false, false ), $this->registry_path( array( 'cache' ), 'object', 'runtime', false, false ), $this->registry_path( array( 'multisite' ), 'object', 'runtime', false, false ), $this->registry_path( array( 'multisite', 'enabled' ), 'boolean', 'runtime', false, false ), ), 'server' => array( $this->registry_path( array( 'php' ), 'object', 'server', false, false ), $this->registry_path( array( 'php', 'version' ), 'string', 'server', false, false ), $this->registry_path( array( 'php', 'sapi' ), 'string', 'server', false, false ), $this->registry_path( array( 'php', 'memory_limit' ), 'string', 'server', false, false ), $this->registry_path( array( 'php', 'max_execution_time' ), 'string', 'server', false, false ), $this->registry_path( array( 'php', 'upload_max_filesize' ), 'string', 'server', false, false ), $this->registry_path( array( 'php', 'post_max_size' ), 'string', 'server', false, false ), $this->registry_path( array( 'php', 'extensions' ), 'object', 'server', false, false ), $this->registry_path( array( 'wordpress' ), 'object', 'server', false, false ), $this->registry_path( array( 'wordpress', 'debug' ), 'boolean', 'server', false, false ), $this->registry_path( array( 'wordpress', 'debug_log' ), 'boolean', 'server', false, false ), $this->registry_path( array( 'wordpress', 'environment' ), 'string', 'server', false, false ), $this->registry_path( array( 'wordpress', 'multisite' ), 'boolean', 'server', false, false ), ), 'site_health' => array( $this->registry_path( array( 'summary' ), 'object', 'site_health', false, false ), $this->registry_path( array( 'summary', 'status' ), 'object', 'site_health', false, false ), $this->registry_path( array( 'summary', 'registered_test_count' ), 'integer', 'site_health', false, false ), $this->registry_path( array( 'summary', 'status_result_count' ), 'integer', 'site_health', false, false ), $this->registry_path( array( 'summary', 'test_count' ), 'integer', 'site_health', false, false ), $this->registry_path( array( 'tests' ), 'array', 'site_health', false, false ), ), 'core' => array( $this->registry_path( array( 'core' ), 'object', 'core', false, false ), $this->registry_path( array( 'core', 'version' ), 'string', 'core', false, false ), $this->registry_path( array( 'core', 'locale' ), 'string', 'core', false, false ), $this->registry_path( array( 'core', 'updates_count' ), 'integer', 'core', false, false ), $this->registry_path( array( 'core', 'update_offers' ), 'array', 'core', false, false ), ), default => array(), }; } /** * Build one read-only registry schema path. */ private function registry_path( array $path, string $type, string $provider, bool $query, bool $order ): array { $entry = array( 'path' => $path, 'type' => $type, 'provider' => $provider, 'read' => true, 'write' => false, 'query' => $query, 'order' => $order, ); if ( $query ) { $entry['operators'] = array( 'eq', 'neq', 'in', 'not_in', 'contains', 'exists' ); } return $entry; } /** * Build effective post type management paths for the current user. * * @param array $providers Provider availability facts. * @return array> */ private function post_types_paths( array $providers ): array { $paths = array( $this->post_types_path( array( 'identity' ), 'object', false, true, true, true ), $this->post_types_path( array( 'identity', 'name' ), 'string', false, true, true, true ), $this->post_types_path( array( 'identity', 'provider' ), 'string', false, true, true, true ), $this->post_types_path( array( 'summary' ), 'object', false, true, true, true ), $this->post_types_path( array( 'summary', 'label' ), 'string', false, true, true, true ), $this->post_types_path( array( 'summary', 'provider' ), 'string', false, true, true, true ), $this->post_types_path( array( 'summary', 'show_in_rest' ), 'boolean', false, true, true, true ), $this->post_types_path( array( 'summary', 'rest_base' ), 'string', false, true, true, true ), $this->post_types_supported_path( $providers, array( 'post_type' ), 'string', false, false, false, 'acf', true ), $this->post_types_supported_path( $providers, array( 'label' ), 'string', false, false, false ), $this->post_types_supported_path( $providers, array( 'plural_label' ), 'string', false, false, false ), $this->post_types_supported_path( $providers, array( 'labels', 'name' ), 'string', true, true, true ), $this->post_types_supported_path( $providers, array( 'labels', 'singular_name' ), 'string', true, true, true ), $this->post_types_supported_path( $providers, array( 'description' ), 'string', false, false, false ), $this->post_types_supported_path( $providers, array( 'public' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'hierarchical' ), 'boolean', true, true, true ), $this->post_types_supported_path( $providers, array( 'show_ui' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'show_in_rest' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'rest_base' ), 'string', false, false, false ), $this->post_types_supported_path( $providers, array( 'supports' ), 'array', true, true, false ), $this->post_types_supported_path( $providers, array( 'taxonomies' ), 'array', true, true, false ), $this->post_types_supported_path( $providers, array( 'has_archive' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'rewrite', 'slug' ), 'string', false, false, false ), $this->post_types_supported_path( $providers, array( 'rewrite', 'with_front' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'rewrite', 'feeds' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'rewrite', 'pages' ), 'boolean', false, false, false ), $this->post_types_supported_path( $providers, array( 'fields' ), 'array', false, false, false ), $this->post_types_supported_path( $providers, array( 'provider_options' ), 'object', false, false, false, 'acf', false, true ), $this->post_types_supported_path( $providers, array( 'provider_options', 'acf' ), 'object', false, false, false, 'acf', false, true ), $this->post_types_supported_path( $providers, array( 'provider_options', 'jetengine' ), 'object', false, false, false, 'jetengine', false, true ), ); return $paths; } /** * Build one post type management schema path. */ private function post_types_path( array $path, string $type, bool $write, bool $read, bool $query, bool $order, string $provider = 'post_types', bool $create = false, bool $assistant_hidden = false ): array { $write = $write && $this->policy_allows_post_type_path_action( 'wp.post_types.update', 'update', $path ); $entry = array( 'path' => $path, 'type' => $type, 'provider' => $provider, 'read' => $read, 'write' => $write, 'query' => $query, 'order' => $order, ); if ( $create ) { $entry['create'] = $this->policy_allows_post_type_path_action( 'wp.post_types.create', 'create', $path ); } if ( $query ) { $entry['operators'] = array( 'eq', 'neq', 'in', 'not_in', 'contains', 'exists' ); } if ( $assistant_hidden ) { $entry['assistant_hidden'] = true; } return $entry; } /** * Build one provider-supported writable post type management schema path. */ private function post_types_supported_path( array $providers, array $path, string $type, bool $read, bool $query, bool $order, string $provider = 'acf', bool $create_only = false, bool $assistant_hidden = false ): array { $write = ! $create_only && $this->post_types_provider_has_path_action( $providers, 'update', $path ); $create = $this->post_types_provider_has_path_action( $providers, 'create', $path ); return $this->post_types_path( $path, $type, $write, $read, $query, $order, $provider, $create, $assistant_hidden ); } /** * Return whether policy allows one post type management path/action. */ private function policy_allows_post_type_path_action( string $capability, string $action, array $path ): bool { $verdict = $this->policy->authorize_current( array( 'capability' => $capability, 'resource' => 'post_types', 'action' => $action, 'touched_paths' => array( $path ), 'wp_capability_allowed' => true, ) ); return true === ( $verdict['allowed'] ?? false ); } /** * Return provider availability facts after applying ActionPanel policy. * * @return array> */ private function post_types_provider_statuses(): array { $providers = PostTypesWriteCapability::provider_statuses(); foreach ( $providers as $index => $provider ) { $actions = array(); if ( true === ( $provider['writable'] ?? false ) ) { if ( $this->policy_allows_post_type_action( 'wp.post_types.create', 'create' ) ) { $actions[] = 'create'; } if ( $this->policy_allows_post_type_action( 'wp.post_types.update', 'update' ) ) { $actions[] = 'update'; } } $providers[ $index ]['actions'] = $actions; if ( true === ( $provider['writable'] ?? false ) && array() === $actions ) { $providers[ $index ]['writable'] = false; $providers[ $index ]['available'] = false; $providers[ $index ]['unavailable_reasons'] = array_values( array_unique( array_merge( isset( $provider['unavailable_reasons'] ) && is_array( $provider['unavailable_reasons'] ) ? $provider['unavailable_reasons'] : array(), array( 'actionpanel_policy_denied' ) ) ) ); } } return $providers; } /** * Return whether any provider exposes one write action. * * @param array> $providers Provider facts. */ private function post_types_provider_has_action( array $providers, string $action ): bool { foreach ( $providers as $provider ) { $actions = isset( $provider['actions'] ) && is_array( $provider['actions'] ) ? array_map( 'strval', $provider['actions'] ) : array(); if ( in_array( $action, $actions, true ) ) { return true; } } return false; } /** * Return whether any writable provider supports one post type path for an action. * * @param array> $providers Provider facts. */ private function post_types_provider_has_path_action( array $providers, string $action, array $path ): bool { $dotted = implode( '.', array_map( 'strval', $path ) ); foreach ( $providers as $provider ) { $actions = isset( $provider['actions'] ) && is_array( $provider['actions'] ) ? array_map( 'strval', $provider['actions'] ) : array(); if ( ! in_array( $action, $actions, true ) ) { continue; } $supported_paths = isset( $provider['supported_paths'] ) && is_array( $provider['supported_paths'] ) ? array_map( 'strval', $provider['supported_paths'] ) : array(); if ( in_array( $dotted, $supported_paths, true ) ) { return true; } } return false; } /** * Return field types supported by the currently writable post type providers. * * @param array> $providers Provider facts. * @return array */ private function post_types_supported_field_types( array $providers ): array { $types = array(); foreach ( $providers as $provider ) { if ( true !== ( $provider['writable'] ?? false ) || ! is_array( $provider['supported_field_types'] ?? null ) ) { continue; } foreach ( $provider['supported_field_types'] as $type ) { if ( is_scalar( $type ) ) { $types[] = (string) $type; } } } return array() === $types ? PostTypesWriteCapability::supported_field_types() : array_values( array_unique( $types ) ); } /** * Return whether policy allows one post type management action. */ private function policy_allows_post_type_action( string $capability, string $action ): bool { $verdict = $this->policy->authorize_current( array( 'capability' => $capability, 'resource' => 'post_types', 'action' => $action, 'touched_paths' => array(), 'wp_capability_allowed' => true, ) ); return true === ( $verdict['allowed'] ?? false ); } /** * Build effective posts paths for the current user. * * @param string $post_type Registered post type. * @return array> */ private function paths_for_posts( string $post_type ): array { $can_read = current_user_can( 'read' ); $can_write = current_user_can( $this->post_type_edit_capability( $post_type ) ); $parent_write = $can_write && $this->post_type_is_hierarchical( $post_type ); $paths = array( $this->path( array( 'id' ), 'integer', 'core', $can_read, false, true, true ), $this->path( array( 'post_type' ), 'string', 'core', $can_read, false, true, true ), $this->path( array( 'status' ), 'string', 'core', $can_read, $can_write, true, true ), $this->path( array( 'slug' ), 'string', 'core', $can_read, $can_write, true, true ), $this->path( array( 'parent' ), 'integer', 'core', $can_read, $parent_write, true, true ), $this->path( array( 'menu_order' ), 'integer', 'core', $can_read, $can_write, true, true ), $this->path( array( 'comment_status' ), 'string', 'core', $can_read, $can_write, true, false ), $this->path( array( 'ping_status' ), 'string', 'core', $can_read, $can_write, true, false ), $this->path( array( 'render_url' ), 'string', 'core', $can_read, false, false, false ), $this->path( array( 'public_url' ), 'string', 'core', $can_read, false, false, false ), $this->path( array( 'preview_url' ), 'string', 'core', $can_read, false, false, false ), $this->path( array( 'title', 'raw' ), 'string', 'core', $can_read, $can_write, true, false ), $this->path( array( 'title', 'rendered' ), 'string', 'core', $can_read, false, false, false ), $this->path( array( 'excerpt', 'raw' ), 'string', 'core', $can_read, $can_write, true, false ), $this->path( array( 'content', 'raw' ), 'string', 'core', $can_read, $can_write, false, false ), $this->path( array( 'blocks' ), 'array', 'core', $can_read, $can_write, false, false ), $this->path( array( 'blocks', '*', 'attrs', '*' ), 'any', 'core', $can_read, $can_write, false, false ), $this->path( array( 'date_gmt' ), 'string', 'core', $can_read, false, true, true ), $this->path( array( 'modified_gmt' ), 'string', 'core', $can_read, false, true, true ), $this->path( array( 'author', 'id' ), 'integer', 'core', $can_read, false, true, true ), ); foreach ( $this->registered_post_meta( $post_type ) as $key => $schema ) { $paths[] = $this->path( array( 'meta', (string) $key ), $this->meta_type( $schema ), 'meta', $can_read, $can_write, true, false ); } foreach ( PostTypesWriteCapability::exposed_acf_fields_for_post_type( $post_type ) as $field ) { $name = isset( $field['name'] ) && is_scalar( $field['name'] ) ? (string) $field['name'] : ''; if ( '' === $name ) { continue; } $paths[] = $this->path( array( 'acf', $name ), PostTypesWriteCapability::schema_type_for_acf_field( $field ), 'acf', $can_read, $can_write, false, false ); } foreach ( PostTypesWriteCapability::exposed_jetengine_fields_for_post_type( $post_type ) as $field ) { $name = isset( $field['name'] ) && is_scalar( $field['name'] ) ? (string) $field['name'] : ''; if ( '' === $name ) { continue; } $paths[] = $this->path( array( 'jetengine', $name ), PostTypesWriteCapability::schema_type_for_jetengine_field( $field ), 'jetengine', $can_read, $can_write, false, false ); } foreach ( $this->registered_taxonomies( $post_type ) as $taxonomy ) { $paths[] = $this->path( array( 'terms', (string) $taxonomy ), 'array', 'terms', $can_read, $can_write, true, false ); } return $paths; } /** * Build one flat path table entry. * * @param array $path Array-segment path. * @param string $type Schema type. * @param string $provider Provider name. * @param bool $read Readable for current user. * @param bool $write Writable for current user. * @param bool $query Queryable. * @param bool $order Orderable. */ private function path( array $path, string $type, string $provider, bool $read, bool $write, bool $query, bool $order ): array { $read = $read && $this->policy_allows_path_action( 'wp.posts.get', 'get', $path ); $write = $write && $this->policy_allows_path_action( 'wp.posts.update', 'update', $path ); $query = $query && $this->policy_allows_path_action( 'wp.posts.query', 'query', $path ); $entry = array( 'path' => $path, 'type' => $type, 'provider' => $provider, 'read' => $read, 'write' => $write, 'query' => $query, 'order' => $order, ); if ( $query ) { $entry['operators'] = PostsQueryCapability::QUERY_OPS; } if ( $this->path_has_prefix( $path, array( 'blocks' ) ) ) { $entry['read_scope'] = 'get'; } return $entry; } private function path_has_prefix( array $path, array $prefix ): bool { if ( count( $path ) < count( $prefix ) ) { return false; } return array_slice( array_values( $path ), 0, count( $prefix ) ) === array_values( $prefix ); } /** * Return whether policy allows one path/action in the schema table. */ private function policy_allows_path_action( string $capability, string $action, array $path ): bool { $verdict = $this->policy->authorize_current( array( 'capability' => $capability, 'resource' => 'posts', 'action' => $action, 'touched_paths' => array( $path ), 'wp_capability_allowed' => true, ) ); return true === ( $verdict['allowed'] ?? false ); } /** * Read registered REST-visible meta for a post subtype. * * @param string $post_type Registered post type. */ private function registered_post_meta( string $post_type ): array { if ( ! function_exists( 'get_registered_meta_keys' ) ) { return array(); } $registered = get_registered_meta_keys( 'post', $post_type ); if ( ! is_array( $registered ) ) { return array(); } return array_filter( $registered, static fn ( $schema ): bool => is_array( $schema ) && ! empty( $schema['show_in_rest'] ) ); } /** * Normalize a registered meta type. * * @param array $schema Registered meta schema. */ private function meta_type( array $schema ): string { $type = isset( $schema['type'] ) && is_scalar( $schema['type'] ) ? (string) $schema['type'] : 'string'; return in_array( $type, self::META_TYPES, true ) ? $type : 'string'; } /** * Return registered taxonomy names for a post type. * * @param string $post_type Registered post type. * @return array */ private function registered_taxonomies( string $post_type ): array { if ( ! function_exists( 'get_object_taxonomies' ) ) { return array(); } $taxonomies = get_object_taxonomies( $post_type, 'names' ); if ( ! is_array( $taxonomies ) ) { return array(); } return array_values( array_map( 'strval', $taxonomies ) ); } /** * Return registered taxonomy names for post type management enum hints. */ private function registered_taxonomies_for_schema(): array { if ( ! function_exists( 'get_taxonomies' ) ) { return array(); } $taxonomies = get_taxonomies( array(), 'names' ); if ( ! is_array( $taxonomies ) ) { return array(); } $names = array_values( array_map( 'strval', $taxonomies ) ); sort( $names ); return $names; } /** * Return registered post type names. * * @return array */ private function registered_post_types(): array { if ( ! function_exists( 'get_post_types' ) ) { return array( 'post', 'page' ); } return array_values( array_map( 'strval', get_post_types( array(), 'names' ) ) ); } /** * Resolve the edit capability for a post type. */ private function post_type_edit_capability( string $post_type ): string { if ( function_exists( 'get_post_type_object' ) ) { $post_type_object = get_post_type_object( $post_type ); if ( is_object( $post_type_object ) && isset( $post_type_object->cap ) ) { return (string) ( $post_type_object->cap->edit_posts ?? 'edit_posts' ); } } return 'page' === $post_type ? 'edit_pages' : 'edit_posts'; } private function post_type_is_hierarchical( string $post_type ): bool { if ( function_exists( 'get_post_type_object' ) ) { $post_type_object = get_post_type_object( $post_type ); if ( is_object( $post_type_object ) && property_exists( $post_type_object, 'hierarchical' ) ) { return (bool) $post_type_object->hierarchical; } } return 'page' === $post_type; } /** * Build enum constraints exposed by the effective post schema. * * @param string $post_type Registered post type. */ private function declared_enums( string $post_type ): array { return array( 'post_type' => $this->registered_post_types(), 'status' => array( 'draft', 'pending', 'private', 'publish', 'future', 'trash' ), 'comment_status' => array( 'open', 'closed' ), 'ping_status' => array( 'open', 'closed' ), 'target' => array( 'post_type' => array( $post_type ) ), ); } /** * Return unique providers represented by a path table. * * @param array $paths Path table entries. * @return array */ private function providers_for( array $paths ): array { $providers = array(); foreach ( $paths as $entry ) { $provider = (string) $entry['provider']; $providers[ $provider ] = $provider; } return array_values( $providers ); } /** * Return whether a value is a path. * * @param mixed $path Candidate path. */ private function is_path( $path ): bool { if ( ! is_array( $path ) || array() === $path ) { return false; } foreach ( $path as $segment ) { if ( ! is_scalar( $segment ) ) { return false; } } return true; } /** * Return whether include contains only known sections. * * @param mixed $sections Candidate include list. */ private function is_include_list( $sections ): bool { if ( ! is_array( $sections ) ) { return false; } foreach ( $sections as $section ) { if ( ! is_scalar( $section ) || ! in_array( (string) $section, self::DEFAULT_SECTIONS, true ) ) { return false; } } return true; } /** * Return whether a path starts with the requested prefix. * * @param array $path Full path. * @param array $prefix Prefix path. */ private static function path_starts_with( array $path, array $prefix ): bool { if ( count( $prefix ) > count( $path ) ) { return false; } foreach ( $prefix as $index => $segment ) { if ( (string) $segment !== (string) $path[ $index ] ) { return false; } } return true; } /** * Build a handler error. * * @param string $code Twelve-code error code. * @param string $message Human/LLM message. * @param array|null $path Error path. * @param array $details Error details. */ private function error( string $code, string $message, ?array $path = null, array $details = array() ): array { $error = array( 'code' => $code, 'message' => $message, ); if ( null !== $path ) { $error['path'] = $path; } if ( array() !== $details ) { $error['details'] = $details; } return array( 'error' => $error ); } }