array( 'resource' => 'templates', 'action' => 'query', ), 'wp.templates.get' => array( 'resource' => 'templates', 'action' => 'get', ), 'wp.templates.update' => array( 'resource' => 'templates', 'action' => 'update', ), 'wp.templates.delete' => array( 'resource' => 'templates', 'action' => 'delete', ), 'wp.template_parts.query' => array( 'resource' => 'template_parts', 'action' => 'query', ), 'wp.template_parts.get' => array( 'resource' => 'template_parts', 'action' => 'get', ), 'wp.template_parts.update' => array( 'resource' => 'template_parts', 'action' => 'update', ), 'wp.template_parts.delete' => array( 'resource' => 'template_parts', 'action' => 'delete', ), 'wp.global_styles.get' => array( 'resource' => 'global_styles', 'action' => 'get', ), 'wp.global_styles.update' => array( 'resource' => 'global_styles', 'action' => 'update', ), ); private string $resource; private string $action; public function __construct( string $capability ) { $metadata = self::CAPABILITIES[ $capability ] ?? array( 'resource' => '', 'action' => '', ); $this->resource = (string) $metadata['resource']; $this->action = (string) $metadata['action']; } /** * Run one theme-identity command. * * @param array $input Command input. * @param string $phase validate or execute. */ public function run( array $input, string $phase ): array { $validation = $this->validate_input( $input ); if ( isset( $validation['error'] ) ) { return $validation; } if ( 'global_styles' === $this->resource ) { return $this->run_global_styles( $input, $phase ); } return match ( $this->action ) { 'query' => 'validate' === $phase ? array( 'data' => array( 'valid' => true ) ) : $this->query_templates( $input ), 'get' => $this->get_template( $input, $phase ), 'update' => $this->update_template( $input, $phase ), 'delete' => $this->delete_template( $input, $phase ), default => $this->error( 'unsupported_capability', 'This theme-identity capability is not supported.' ), }; } private function validate_input( array $input ): array { if ( isset( $input['select'] ) && ! $this->is_path_list( $input['select'] ) ) { return $this->error( 'invalid_command', 'select must be a list of array-segment paths.', array( 'input', 'select' ) ); } if ( isset( $input['limit'] ) ) { if ( ! is_numeric( $input['limit'] ) || (int) $input['limit'] < 1 ) { return $this->error( 'invalid_value', 'limit must be a positive integer.', array( 'input', 'limit' ) ); } if ( (int) $input['limit'] > self::MAX_LIMIT ) { return $this->error( 'limit_exceeded', 'limit exceeds the theme-identity query cap.', array( 'input', 'limit' ), array( 'max' => self::MAX_LIMIT ) ); } } if ( isset( $input['cursor'] ) && null !== $input['cursor'] && ! is_string( $input['cursor'] ) ) { return $this->error( 'invalid_value', 'cursor must be null or an opaque string.', array( 'input', 'cursor' ) ); } if ( 'query' === $this->action ) { $cursor = CommandPagination::validate( $input['cursor'] ?? null, $this->cursor_context( $input ) ); if ( isset( $cursor['error'] ) ) { return $cursor; } $query_shape = $this->validate_query_shape( $input ); if ( isset( $query_shape['error'] ) ) { return $query_shape; } } if ( $this->is_write_action() && ! current_user_can( 'edit_theme_options' ) ) { return $this->error( 'permission_denied', 'The current user cannot edit theme identity resources.', array( 'input' ), array( 'layer' => 'wp_capability' ) ); } return array( 'data' => array( 'valid' => true ) ); } private function is_write_action(): bool { return in_array( $this->action, array( 'update', 'delete' ), true ); } private function run_global_styles( array $input, string $phase ): array { if ( 'get' === $this->action ) { if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $document = $this->global_styles_document(); return array( 'data' => array( 'resource' => 'global_styles', 'target' => array( 'theme' => $this->target_theme( $input['target'] ?? array() ) ), 'document' => $this->project_document( $document, $input['select'] ?? null ), 'facts' => $this->facts_for_content( $this->encode_json( $document ) ), ), ); } $changes = $this->changes_from_input( $input ); if ( array() === $changes ) { return $this->error( 'invalid_command', 'global_styles.update requires a non-empty changes list.', array( 'input', 'changes' ) ); } $result = $this->apply_changes( $this->global_styles_document(), $changes, false ); if ( isset( $result['error'] ) ) { return $result; } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $document = $result['document']; $this->persist_global_styles_document( $document ); return array( 'data' => array( 'updated' => true, 'target' => array( 'resource' => 'global_styles', 'theme' => $this->target_theme( $input['target'] ?? array() ), ), ), ); } private function query_templates( array $input ): array { $documents = $this->template_documents( $this->target_theme( $input['target'] ?? array() ) ); $documents = $this->filter_documents( $documents, $input['where'] ?? null, $input['search'] ?? null ); $documents = $this->order_documents( $documents, $input['order_by'] ?? null ); $limit = isset( $input['limit'] ) ? (int) $input['limit'] : self::DEFAULT_LIMIT; $page = CommandPagination::page( $documents, $limit, $input['cursor'] ?? null, $this->cursor_context( $input ) ); if ( isset( $page['error'] ) ) { return $page; } return array( 'data' => array( 'resource' => $this->resource, 'items' => array_map( fn ( array $document ): array => array( 'target' => $this->target_for_document( $document ), 'document' => $this->project_document( $document, $input['select'] ?? null ), 'facts' => $this->facts_for_content( (string) ( $document['content']['raw'] ?? '' ) ), ), $page['items'] ), 'page' => array( 'next_cursor' => $page['next_cursor'], 'limit' => $limit, ), ), 'meta' => array( 'total' => count( $documents ), 'returned_count' => count( $page['items'] ), ), ); } private function cursor_context( array $input ): array { return array( 'resource' => $this->resource, 'where' => $input['where'] ?? null, 'search' => $input['search'] ?? null, 'order_by' => $input['order_by'] ?? null, ); } private function get_template( array $input, string $phase ): array { $document = $this->template_document_for_target( $input['target'] ?? array() ); if ( isset( $document['error'] ) ) { return $document; } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } return array( 'data' => array( 'resource' => $this->resource, 'target' => $this->target_for_document( $document ), 'document' => $this->project_document( $document, $input['select'] ?? null ), 'facts' => $this->facts_for_content( (string) ( $document['content']['raw'] ?? '' ) ), ), ); } private function update_template( array $input, string $phase ): array { $document = $this->template_document_for_target( $input['target'] ?? array() ); if ( isset( $document['error'] ) ) { return $document; } $preconditions = $this->validate_preconditions( (string) ( $document['content']['raw'] ?? '' ), $input['target'] ?? array() ); if ( isset( $preconditions['error'] ) ) { return $preconditions; } $changes = $this->changes_from_input( $input ); if ( array() === $changes ) { return $this->error( 'invalid_command', 'Template update requires a non-empty changes list.', array( 'input', 'changes' ) ); } $result = $this->apply_changes( $document, $changes, true ); if ( isset( $result['error'] ) ) { return $result; } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $document = $result['document']; $this->persist_template_customization( $document ); return array( 'data' => array( 'updated' => true, 'customized' => true, 'target' => $this->target_for_document( $document ), ), ); } private function delete_template( array $input, string $phase ): array { $document = $this->template_document_for_target( $input['target'] ?? array() ); if ( isset( $document['error'] ) ) { return $document; } $preconditions = $this->validate_preconditions( (string) ( $document['content']['raw'] ?? '' ), $input['target'] ?? array() ); if ( isset( $preconditions['error'] ) ) { return $preconditions; } if ( 'validate' === $phase ) { return array( 'data' => array( 'valid' => true ) ); } $this->delete_template_customization( $document ); return array( 'data' => array( 'reverted' => true, 'target' => $this->target_for_document( $document ), ), ); } private function template_document_for_target( $target ): array { $identity = $this->template_identity( is_array( $target ) ? $target : array() ); if ( isset( $identity['error'] ) ) { return $identity; } foreach ( $this->template_documents( $identity['theme'] ) as $document ) { if ( $identity['id'] === $document['identity']['id'] ) { return $document; } } return $this->error( 'not_found', 'No template matched the requested theme identity.', array( 'input', 'target' ) ); } private function template_documents( string $theme ): array { $this->ensure_block_template_functions(); $template_type = $this->template_type(); $templates = $this->get_block_templates( array( 'theme' => $theme ), $template_type ); if ( ! is_array( $templates ) ) { return array(); } $documents = array(); foreach ( $templates as $template ) { if ( ! is_object( $template ) ) { continue; } $slug = isset( $template->slug ) && is_scalar( $template->slug ) ? (string) $template->slug : ''; if ( '' === $slug ) { continue; } $source = isset( $template->source ) && is_scalar( $template->source ) ? (string) $template->source : 'theme'; $content = isset( $template->content ) && is_scalar( $template->content ) ? (string) $template->content : ''; $document = array( 'identity' => array( 'id' => $theme . '//' . $slug, 'theme' => $theme, 'slug' => $slug, 'type' => $template_type, ), 'title' => array( 'raw' => isset( $template->title ) && is_scalar( $template->title ) ? (string) $template->title : $slug ), 'description' => array( 'raw' => isset( $template->description ) && is_scalar( $template->description ) ? (string) $template->description : '' ), 'area' => isset( $template->area ) && is_scalar( $template->area ) ? (string) $template->area : null, 'content' => array( 'raw' => $content ), 'blocks' => $this->blocks_document( $content ), 'status' => array( 'customized' => 'theme' !== $source, 'source' => 'theme' === $source ? 'theme' : 'custom', ), ); $documents[] = $document; } return $documents; } private function get_block_templates( array $query, string $template_type ): array { if ( function_exists( 'wp_get_block_templates' ) ) { return wp_get_block_templates( $query, $template_type ); } if ( function_exists( 'get_block_templates' ) ) { return get_block_templates( $query, $template_type ); } return array(); } private function ensure_block_template_functions(): void { if ( function_exists( 'wp_get_block_templates' ) || function_exists( 'get_block_templates' ) ) { return; } if ( defined( 'ABSPATH' ) && defined( 'WPINC' ) ) { $path = ABSPATH . WPINC . '/block-template-utils.php'; if ( is_readable( $path ) ) { require_once $path; } } } private function filter_documents( array $documents, $where, $search ): array { $filtered = $documents; if ( is_array( $where ) && array() !== $where ) { $filtered = array_values( array_filter( $filtered, fn ( array $document ): bool => $this->matches_where( $document, $where ) ) ); } if ( is_scalar( $search ) && '' !== (string) $search ) { $needle = strtolower( (string) $search ); $filtered = array_values( array_filter( $filtered, static fn ( array $document ): bool => false !== strpos( strtolower( (string) json_encode( $document ) ), $needle ) ) ); } return $filtered; } private function matches_where( array $document, array $where ): bool { if ( isset( $where['all'] ) && is_array( $where['all'] ) ) { foreach ( $where['all'] as $predicate ) { if ( ! is_array( $predicate ) || ! $this->matches_where( $document, $predicate ) ) { return false; } } return true; } if ( isset( $where['any'] ) && is_array( $where['any'] ) ) { foreach ( $where['any'] as $predicate ) { if ( is_array( $predicate ) && $this->matches_where( $document, $predicate ) ) { return true; } } return false; } if ( isset( $where['path'] ) && is_array( $where['path'] ) ) { $op = isset( $where['op'] ) && is_scalar( $where['op'] ) ? (string) $where['op'] : 'eq'; return $this->predicate_matches( $this->path_get( $document, array_values( $where['path'] ) ), $op, $where['value'] ?? null ); } return true; } private function predicate_matches( $actual, string $op, $expected ): bool { return match ( $op ) { 'eq' => $actual === $expected, 'neq' => $actual !== $expected, 'in' => is_array( $expected ) && in_array( $actual, $expected, true ), 'not_in' => is_array( $expected ) && ! in_array( $actual, $expected, true ), 'contains' => is_scalar( $actual ) && is_scalar( $expected ) && false !== strpos( (string) $actual, (string) $expected ), 'exists' => null !== $actual, default => false, }; } /** * Reject query shapes the runtime does not evaluate. * * The effective schema advertises the queryable/orderable paths and the * operator vocabulary; anything outside that truth fails here instead of * silently matching as `eq` or silently skipping ordering. */ private function validate_query_shape( array $input ): array { if ( isset( $input['where'] ) && is_array( $input['where'] ) && array() !== $input['where'] ) { $where = $this->validate_where_group( $input['where'], array( 'input', 'where' ) ); if ( isset( $where['error'] ) ) { return $where; } } if ( ! isset( $input['order_by'] ) ) { return array( 'data' => array( 'valid' => true ) ); } if ( ! is_array( $input['order_by'] ) ) { return $this->error( 'invalid_command', 'order_by must be a list of {path, dir} entries.', array( 'input', 'order_by' ) ); } foreach ( array_values( $input['order_by'] ) as $index => $order ) { if ( ! is_array( $order ) || ! isset( $order['path'] ) || ! is_array( $order['path'] ) ) { return $this->error( 'invalid_command', 'order_by entries require an array-segment path.', array( 'input', 'order_by', $index ) ); } $key = implode( '.', array_map( 'strval', array_values( $order['path'] ) ) ); if ( ! in_array( $key, self::ORDERABLE_PATHS, true ) ) { return $this->error( 'invalid_path', 'order_by path is not orderable for this resource.', array( 'input', 'order_by', $index, 'path' ) ); } $dir = isset( $order['dir'] ) && is_scalar( $order['dir'] ) ? strtolower( (string) $order['dir'] ) : 'asc'; if ( ! in_array( $dir, array( 'asc', 'desc' ), true ) ) { return $this->error( 'invalid_value', 'order_by dir must be asc or desc.', array( 'input', 'order_by', $index, 'dir' ) ); } } return array( 'data' => array( 'valid' => true ) ); } private function validate_where_group( array $where, array $path ): array { foreach ( array( 'all', 'any' ) as $group ) { if ( ! isset( $where[ $group ] ) ) { continue; } if ( ! is_array( $where[ $group ] ) ) { return $this->error( 'invalid_command', 'where groups must be lists of predicates.', array_merge( $path, array( $group ) ) ); } foreach ( array_values( $where[ $group ] ) as $index => $predicate ) { if ( ! is_array( $predicate ) ) { return $this->error( 'invalid_command', 'where predicates must be objects.', array_merge( $path, array( $group, $index ) ) ); } $nested = $this->validate_where_group( $predicate, array_merge( $path, array( $group, $index ) ) ); if ( isset( $nested['error'] ) ) { return $nested; } } } if ( ! isset( $where['all'] ) && ! isset( $where['any'] ) ) { if ( ! isset( $where['path'] ) || ! is_array( $where['path'] ) ) { return $this->error( 'invalid_command', 'where predicates require an array-segment path.', $path ); } $key = implode( '.', array_map( 'strval', array_values( $where['path'] ) ) ); if ( ! in_array( $key, self::QUERYABLE_PATHS, true ) ) { return $this->error( 'invalid_path', 'where path is not queryable for this resource.', array_merge( $path, array( 'path' ) ) ); } $op = isset( $where['op'] ) && is_scalar( $where['op'] ) ? (string) $where['op'] : 'eq'; if ( ! in_array( $op, self::QUERY_OPS, true ) ) { return $this->error( 'invalid_value', 'where op is not supported by theme-identity queries.', array_merge( $path, array( 'op' ) ), array( 'supported' => self::QUERY_OPS ) ); } } return array( 'data' => array( 'valid' => true ) ); } /** * Sort documents by the validated order_by list. * * @param array $documents Filtered documents. * @param mixed $order_by Validated order_by input. */ private function order_documents( array $documents, $order_by ): array { if ( ! is_array( $order_by ) || array() === $order_by ) { return $documents; } $orders = array(); foreach ( $order_by as $order ) { $orders[] = array( 'path' => array_values( $order['path'] ), 'desc' => 'desc' === strtolower( (string) ( $order['dir'] ?? 'asc' ) ), ); } usort( $documents, function ( array $left, array $right ) use ( $orders ): int { foreach ( $orders as $order ) { $comparison = $this->compare_order_values( $this->path_get( $left, $order['path'] ), $this->path_get( $right, $order['path'] ) ); if ( 0 !== $comparison ) { return $order['desc'] ? -$comparison : $comparison; } } return 0; } ); return array_values( $documents ); } private function compare_order_values( $left, $right ): int { if ( is_numeric( $left ) && is_numeric( $right ) ) { return (float) $left <=> (float) $right; } $left_text = is_scalar( $left ) ? (string) $left : ''; $right_text = is_scalar( $right ) ? (string) $right : ''; return strcasecmp( $left_text, $right_text ) <=> 0; } private function template_identity( array $target ): array { $id = isset( $target['id'] ) && is_scalar( $target['id'] ) ? (string) $target['id'] : ''; if ( '' !== $id ) { $parts = explode( '//', $id, 2 ); if ( 2 === count( $parts ) && '' !== $parts[0] && '' !== $parts[1] ) { return array( 'id' => $id, 'theme' => $parts[0], 'slug' => $parts[1], ); } } $theme = isset( $target['theme'] ) && is_scalar( $target['theme'] ) ? (string) $target['theme'] : $this->active_theme(); $slug = isset( $target['slug'] ) && is_scalar( $target['slug'] ) ? (string) $target['slug'] : ''; if ( '' === $theme || '' === $slug ) { return $this->error( 'invalid_target', 'Template targets require id as theme//slug or theme plus slug.', array( 'input', 'target' ) ); } return array( 'id' => $theme . '//' . $slug, 'theme' => $theme, 'slug' => $slug, ); } private function changes_from_input( array $input ): array { return isset( $input['changes'] ) && is_array( $input['changes'] ) ? array_values( $input['changes'] ) : array(); } private function target_theme( $target ): string { if ( is_array( $target ) && isset( $target['theme'] ) && is_scalar( $target['theme'] ) && '' !== (string) $target['theme'] ) { return (string) $target['theme']; } return $this->active_theme(); } private function target_for_document( array $document ): array { $identity = isset( $document['identity'] ) && is_array( $document['identity'] ) ? $document['identity'] : array(); return array( 'resource' => $this->resource, 'id' => (string) ( $identity['id'] ?? '' ), 'theme' => (string) ( $identity['theme'] ?? '' ), 'slug' => (string) ( $identity['slug'] ?? '' ), ); } private function template_type(): string { return 'templates' === $this->resource ? 'wp_template' : 'wp_template_part'; } private function active_theme(): string { if ( function_exists( 'get_stylesheet' ) ) { $stylesheet = get_stylesheet(); if ( is_scalar( $stylesheet ) && '' !== (string) $stylesheet ) { return (string) $stylesheet; } } if ( function_exists( 'wp_get_theme' ) ) { $theme = wp_get_theme(); if ( is_object( $theme ) && method_exists( $theme, 'get_stylesheet' ) ) { return (string) $theme->get_stylesheet(); } } return ''; } private function project_document( array $document, $select ): array { if ( ! is_array( $select ) || array() === $select ) { return $this->default_projection( $document ); } $projected = array(); foreach ( $select as $path ) { $segments = array_values( $path ); if ( ! $this->path_exists( $document, $segments ) ) { continue; } $projected = $this->path_set( $projected, $segments, $this->path_get( $document, $segments ) ); } return $projected; } private function default_projection( array $document ): array { if ( 'global_styles' === $this->resource ) { return $document; } return array( 'identity' => $document['identity'], 'title' => $document['title'], 'status' => $document['status'], ); } private function apply_change( array $document, array $change, int $index ): array { $op = isset( $change['op'] ) && is_scalar( $change['op'] ) ? (string) $change['op'] : ''; if ( ! in_array( $op, array( 'set', 'remove', 'insert' ), true ) ) { return $this->error( 'invalid_command', 'Change op must be set, remove, or insert.', array( 'input', 'changes', $index, 'op' ) ); } if ( ! isset( $change['path'] ) || ! $this->is_path( $change['path'] ) ) { return $this->error( 'invalid_path', 'Change path must be an array-segment path.', array( 'input', 'changes', $index, 'path' ) ); } $path = array_values( $change['path'] ); if ( 'remove' !== $op && ! array_key_exists( 'value', $change ) ) { return $this->error( 'invalid_command', 'set and insert changes require value.', array( 'input', 'changes', $index, 'value' ) ); } if ( 'set' === $op ) { return array( 'document' => $this->path_set( $document, $path, $change['value'] ) ); } if ( 'remove' === $op ) { if ( ! $this->path_exists( $document, $path ) ) { return $this->error( 'invalid_path', 'Cannot remove a path that does not exist.', array( 'input', 'changes', $index, 'path' ) ); } return array( 'document' => $this->path_remove( $document, $path ) ); } if ( ! $this->path_exists( $document, array_slice( $path, 0, -1 ) ) ) { return $this->error( 'invalid_path', 'Cannot insert into a path that does not exist.', array( 'input', 'changes', $index, 'path' ) ); } return array( 'document' => $this->path_insert( $document, $path, $change['value'] ) ); } private function apply_changes( array $document, array $changes, bool $sync_template_blocks ): array { foreach ( $changes as $index => $change ) { $result = $this->apply_change( $document, is_array( $change ) ? $change : array(), $index ); if ( isset( $result['error'] ) ) { return $result; } $document = true === $sync_template_blocks ? $this->sync_template_blocks( $result['document'] ) : $result['document']; } return array( 'document' => $document ); } private function sync_template_blocks( array $document ): array { if ( isset( $document['content']['raw'] ) && is_scalar( $document['content']['raw'] ) ) { $document['blocks'] = $this->blocks_document( (string) $document['content']['raw'] ); return $document; } if ( isset( $document['blocks'] ) && is_array( $document['blocks'] ) ) { $document['content']['raw'] = implode( '', array_map( fn ( array $block ): string => (string) ( $block['markup'] ?? '' ), $document['blocks'] ) ); } return $document; } private function blocks_document( string $content ): array { if ( ! function_exists( 'parse_blocks' ) ) { return array(); } $blocks = parse_blocks( $content ); return is_array( $blocks ) ? $this->project_blocks( $blocks ) : array(); } private function project_blocks( array $blocks ): array { $projected = array(); foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } $inner_blocks = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : array(); $projected[] = array( 'blockName' => isset( $block['blockName'] ) && is_scalar( $block['blockName'] ) ? (string) $block['blockName'] : null, 'attrs' => isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(), 'markup' => function_exists( 'serialize_block' ) ? serialize_block( $block ) : '', 'innerBlocks' => $this->project_blocks( $inner_blocks ), ); } return $projected; } private function validate_preconditions( string $content, $target ): array { if ( ! is_array( $target ) || ! isset( $target['if'] ) || ! is_array( $target['if'] ) ) { return array( 'data' => array( 'valid' => true ) ); } $expected = $target['if']; $current = $this->facts_for_content( $content ); if ( isset( $expected['content_hash'] ) && (string) $expected['content_hash'] !== $current['content_hash'] ) { return $this->error( 'precondition_failed', 'Target precondition did not match the current template state.', array( 'input', 'target', 'if' ), array( 'expected' => array( 'content_hash' => (string) $expected['content_hash'] ), 'current' => $current, ) ); } return array( 'data' => array( 'valid' => true ) ); } private function facts_for_content( string $content ): array { return array( 'content_hash' => self::content_hash( $content ), 'theme' => $this->active_theme(), ); } public static function content_hash( string $content ): string { return 'r_' . substr( md5( $content ), 0, 12 ); } private function persist_template_customization( array $document ): void { $identity = $document['identity']; if ( function_exists( 'command_contract_save_block_template_customization' ) ) { command_contract_save_block_template_customization( $this->template_type(), (string) $identity['theme'], (string) $identity['slug'], array( 'title' => $document['title']['raw'] ?? '', 'description' => $document['description']['raw'] ?? '', 'area' => $document['area'] ?? null, 'content' => $document['content']['raw'] ?? '', ) ); return; } if ( function_exists( 'wp_insert_post' ) ) { wp_insert_post( array( 'post_type' => $this->template_type(), 'post_status' => 'publish', 'post_name' => (string) $identity['slug'], 'post_title' => (string) ( $document['title']['raw'] ?? $identity['slug'] ), 'post_content' => (string) ( $document['content']['raw'] ?? '' ), 'tax_input' => array( 'wp_theme' => (string) $identity['theme'] ), ), true ); } } private function delete_template_customization( array $document ): void { $identity = $document['identity']; if ( function_exists( 'command_contract_delete_block_template_customization' ) ) { command_contract_delete_block_template_customization( $this->template_type(), (string) $identity['theme'], (string) $identity['slug'] ); return; } $custom = $this->find_template_customization_post( (string) $identity['theme'], (string) $identity['slug'] ); if ( null !== $custom && function_exists( 'wp_delete_post' ) ) { wp_delete_post( (int) $custom->ID, true ); } } private function find_template_customization_post( string $theme, string $slug ): ?object { if ( ! function_exists( 'get_posts' ) ) { return null; } $posts = get_posts( array( 'post_type' => $this->template_type(), 'name' => $slug, 'post_status' => array( 'publish', 'draft', 'auto-draft' ), 'posts_per_page' => 1, // Same wp_theme taxonomy lookup core uses to resolve block // templates; single post, single term. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 'tax_query' => array( array( 'taxonomy' => 'wp_theme', 'field' => 'name', 'terms' => $theme, ), ), ) ); return is_array( $posts ) && isset( $posts[0] ) && is_object( $posts[0] ) ? $posts[0] : null; } private function global_styles_document(): array { if ( class_exists( '\WP_Theme_JSON_Resolver' ) && method_exists( '\WP_Theme_JSON_Resolver', 'get_merged_data' ) ) { $theme_json = \WP_Theme_JSON_Resolver::get_merged_data(); if ( is_object( $theme_json ) && method_exists( $theme_json, 'get_raw_data' ) ) { $document = $theme_json->get_raw_data(); if ( is_array( $document ) ) { return $document; } } } if ( function_exists( 'wp_get_global_styles' ) ) { $styles = wp_get_global_styles(); if ( is_array( $styles ) ) { return array( 'styles' => $styles ); } } return array(); } private function persist_global_styles_document( array $document ): void { if ( function_exists( 'command_contract_update_global_styles' ) ) { command_contract_update_global_styles( $document ); return; } if ( class_exists( '\WP_Theme_JSON_Resolver' ) && function_exists( 'wp_get_theme' ) && function_exists( 'wp_update_post' ) ) { $user_data = \WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), true, array( 'publish', 'draft' ) ); $post_id = isset( $user_data['ID'] ) ? (int) $user_data['ID'] : 0; if ( $post_id > 0 ) { wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish', 'post_content' => $this->encode_json( $this->prepare_global_styles_user_document( $document ) ), ), true ); if ( function_exists( 'clean_post_cache' ) ) { clean_post_cache( $post_id ); } $this->clean_theme_json_cache(); return; } } if ( function_exists( 'wp_insert_post' ) ) { wp_insert_post( array( 'post_type' => 'wp_global_styles', 'post_status' => 'publish', 'post_name' => 'wp-global-styles-' . rawurlencode( $this->active_theme() ), 'post_title' => 'Global Styles', 'post_content' => $this->encode_json( $this->prepare_global_styles_user_document( $document ) ), 'tax_input' => array( 'wp_theme' => array( $this->active_theme() ) ), ), true ); $this->clean_theme_json_cache(); } } private function clean_theme_json_cache(): void { if ( class_exists( '\WP_Theme_JSON_Resolver' ) && method_exists( '\WP_Theme_JSON_Resolver', 'clean_cached_data' ) ) { \WP_Theme_JSON_Resolver::clean_cached_data(); } } private function prepare_global_styles_user_document( array $document ): array { $document['isGlobalStylesUserThemeJSON'] = true; if ( ! isset( $document['version'] ) ) { $document['version'] = 3; } return $document; } private function encode_json( $value ): string { return function_exists( 'wp_json_encode' ) ? (string) wp_json_encode( $value ) : (string) json_encode( $value ); } private function path_exists( $document, array $segments ): bool { $node = $document; foreach ( $segments as $segment ) { $key = is_scalar( $segment ) ? (string) $segment : ''; if ( ! is_array( $node ) || ! array_key_exists( $key, $node ) ) { return false; } $node = $node[ $key ]; } return true; } private function path_get( $document, array $segments ) { $node = $document; foreach ( $segments as $segment ) { $key = is_scalar( $segment ) ? (string) $segment : ''; if ( ! is_array( $node ) || ! array_key_exists( $key, $node ) ) { return null; } $node = $node[ $key ]; } return $node; } private function path_set( array $document, array $segments, $value ): array { $key = (string) $segments[0]; if ( 1 === count( $segments ) ) { $document[ $key ] = $value; return $document; } $child = isset( $document[ $key ] ) && is_array( $document[ $key ] ) ? $document[ $key ] : array(); $document[ $key ] = $this->path_set( $child, array_slice( $segments, 1 ), $value ); return $document; } private function path_remove( array $document, array $segments ): array { $key = (string) $segments[0]; if ( 1 === count( $segments ) ) { unset( $document[ $key ] ); return $document; } if ( isset( $document[ $key ] ) && is_array( $document[ $key ] ) ) { $document[ $key ] = $this->path_remove( $document[ $key ], array_slice( $segments, 1 ) ); } return $document; } private function path_insert( array $document, array $segments, $value ): array { if ( 1 === count( $segments ) ) { return $document; } $index_path = array_slice( $segments, 0, -1 ); $position = end( $segments ); $list = $this->path_get( $document, $index_path ); $list = is_array( $list ) ? array_values( $list ) : array(); if ( '-' === $position ) { $list[] = $value; } else { array_splice( $list, max( 0, (int) $position ), 0, array( $value ) ); } return $this->path_set( $document, $index_path, $list ); } private function is_path_list( $select ): bool { if ( ! is_array( $select ) ) { return false; } foreach ( $select as $path ) { if ( ! $this->is_path( $path ) ) { return false; } } return true; } 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; } 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 ); } }