class-avcf-abilities-base.php
3 weeks ago
class-avcf-abilities-block-navigation.php
3 weeks ago
class-avcf-abilities-cache.php
3 weeks ago
class-avcf-abilities-content.php
3 weeks ago
class-avcf-abilities-core.php
3 weeks ago
class-avcf-abilities-global-styles.php
3 weeks ago
class-avcf-abilities-gutenberg.php
3 weeks ago
class-avcf-abilities-media.php
3 weeks ago
class-avcf-abilities-metadata.php
3 weeks ago
class-avcf-abilities-navigation.php
3 weeks ago
class-avcf-abilities-patterns.php
3 weeks ago
class-avcf-abilities-plugins.php
3 weeks ago
class-avcf-abilities-settings.php
3 weeks ago
class-avcf-abilities-taxonomies.php
3 weeks ago
class-avcf-abilities-templates.php
3 weeks ago
class-avcf-abilities-theme-files.php
3 weeks ago
class-avcf-abilities-themes.php
3 weeks ago
class-avcf-abilities-users.php
3 weeks ago
class-avcf-abilities-templates.php
520 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Block-theme templates and template-part MCP abilities (Full Site Editing). |
| 4 | * |
| 5 | * In a block theme the site-wide chrome — the header, footer, and the layout |
| 6 | * of the home / single / archive / 404 screens — lives in block TEMPLATES and |
| 7 | * TEMPLATE PARTS, not in posts, classic menus, or widgets. These are file-based |
| 8 | * in the active theme until a user customises one, at which point WordPress |
| 9 | * stores an overriding row in the wp_template / wp_template_part post types. |
| 10 | * |
| 11 | * The generic content abilities cannot reach these: list-post-types only |
| 12 | * surfaces public types, and list-content returns nothing for wp_template |
| 13 | * because file-based templates are not database rows. This category exposes |
| 14 | * them explicitly so the AI can read and edit the parts of a site that define |
| 15 | * its overall design. |
| 16 | * |
| 17 | * A single set of tools handles both template types via a `type` discriminator |
| 18 | * ("wp_template" | "wp_template_part"), mirroring how core's Site Editor treats |
| 19 | * them. Writes upsert an overriding post attached to the active theme's |
| 20 | * wp_theme term (validated against WordPress core template resolution); revert |
| 21 | * deletes that override so the theme file takes over again. |
| 22 | * |
| 23 | * Exposed abilities: |
| 24 | * atarim/get-site-editor-overview Discovery: is-block-theme, template/part/pattern/navigation counts. |
| 25 | * atarim/list-templates All templates or parts with source (theme/custom) + area. |
| 26 | * atarim/get-template One template's block markup + top-level block summary. |
| 27 | * atarim/update-template Upsert a template's block content (overrides the theme file). |
| 28 | * atarim/revert-template Delete a customisation so the theme file is used again. |
| 29 | * |
| 30 | * @package atarim-visual-collaboration |
| 31 | */ |
| 32 | |
| 33 | if ( ! defined('ABSPATH') ) { |
| 34 | exit; |
| 35 | } |
| 36 | |
| 37 | class AVCF_Abilities_Templates extends AVCF_Abilities_Base { |
| 38 | |
| 39 | private const TEMPLATE_TYPES = [ 'wp_template', 'wp_template_part' ]; |
| 40 | |
| 41 | public function register() { |
| 42 | $this->register_overview(); |
| 43 | $this->register_list(); |
| 44 | $this->register_get(); |
| 45 | $this->register_update(); |
| 46 | $this->register_revert(); |
| 47 | } |
| 48 | |
| 49 | private function register_overview() { |
| 50 | wp_register_ability( 'atarim/get-site-editor-overview', [ |
| 51 | 'label' => 'Get Site Editor Overview', |
| 52 | 'description' => 'Discovery entry point for the Full Site Editing layer that the generic content tools cannot see. Reports whether the active theme is a block theme, the active theme name, and counts + identifiers for block templates (home/single/archive/404/etc.), template parts (header/footer/sidebar), registered block patterns and their categories, wp_navigation menus, and whether global styles have been customised. Call this first when building or restyling a block-theme site to learn what design surfaces exist, then use list-templates / list-patterns / get-global-styles / list-navigation to go deeper.', |
| 53 | 'category' => 'atarim', |
| 54 | 'input_schema' => [ |
| 55 | 'type' => 'object', |
| 56 | 'properties' => [], |
| 57 | 'additionalProperties' => false, |
| 58 | ], |
| 59 | 'output_schema' => [ |
| 60 | 'type' => 'object', |
| 61 | 'properties' => [ |
| 62 | 'success' => [ 'type' => 'boolean' ], |
| 63 | 'is_block_theme' => [ 'type' => 'boolean' ], |
| 64 | 'active_theme' => [ 'type' => 'string' ], |
| 65 | 'templates' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 66 | 'template_parts' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 67 | 'pattern_count' => [ 'type' => 'integer' ], |
| 68 | 'pattern_categories' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], |
| 69 | 'navigation_menu_count' => [ 'type' => 'integer' ], |
| 70 | 'global_styles_customised' => [ 'type' => 'boolean' ], |
| 71 | 'message' => [ 'type' => 'string' ], |
| 72 | ], |
| 73 | 'required' => [ 'success', 'message' ], |
| 74 | ], |
| 75 | 'execute_callback' => function( $input = [] ) { |
| 76 | $is_block = function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(); |
| 77 | $theme = wp_get_theme(); |
| 78 | |
| 79 | $templates = $this->avcf_template_slugs( 'wp_template' ); |
| 80 | $parts = $this->avcf_template_slugs( 'wp_template_part' ); |
| 81 | |
| 82 | $pattern_categories = []; |
| 83 | if ( class_exists( 'WP_Block_Pattern_Categories_Registry' ) ) { |
| 84 | foreach ( WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered() as $cat ) { |
| 85 | $pattern_categories[] = (string) $cat['name']; |
| 86 | } |
| 87 | } |
| 88 | $pattern_count = class_exists( 'WP_Block_Patterns_Registry' ) |
| 89 | ? count( WP_Block_Patterns_Registry::get_instance()->get_all_registered() ) |
| 90 | : 0; |
| 91 | |
| 92 | $nav_count = (int) wp_count_posts( 'wp_navigation' )->publish; |
| 93 | $gs_custom = $this->avcf_global_styles_customised(); |
| 94 | |
| 95 | return [ |
| 96 | 'success' => true, |
| 97 | 'is_block_theme' => $is_block, |
| 98 | 'active_theme' => (string) $theme->get( 'Name' ), |
| 99 | 'templates' => $templates, |
| 100 | 'template_parts' => $parts, |
| 101 | 'pattern_count' => $pattern_count, |
| 102 | 'pattern_categories' => $pattern_categories, |
| 103 | 'navigation_menu_count' => $nav_count, |
| 104 | 'global_styles_customised' => $gs_custom, |
| 105 | 'message' => $is_block |
| 106 | ? sprintf( '%s (block theme): %d templates, %d parts, %d patterns, %d navigation menu(s).', $theme->get( 'Name' ), count( $templates ), count( $parts ), $pattern_count, $nav_count ) |
| 107 | : sprintf( '%s is a classic theme — templates/global-styles are limited; patterns and classic menus/widgets apply instead.', $theme->get( 'Name' ) ), |
| 108 | ]; |
| 109 | }, |
| 110 | 'permission_callback' => function() { |
| 111 | return current_user_can( 'edit_theme_options' ); |
| 112 | }, |
| 113 | 'meta' => [ |
| 114 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 115 | 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ], |
| 116 | ], |
| 117 | ] ); |
| 118 | } |
| 119 | |
| 120 | private function register_list() { |
| 121 | wp_register_ability( 'atarim/list-templates', [ |
| 122 | 'label' => 'List Templates', |
| 123 | 'description' => 'Returns block-theme templates or template parts. type controls which: "wp_template" (default) for full-page templates (home, index, single, page, archive, search, 404, and custom page-* templates), or "wp_template_part" for reusable regions (header, footer, sidebar). Each item includes id ("stylesheet//slug", used by get/update/revert-template), slug, title, description, source ("theme" = still the unmodified theme file, "custom" = customised/overridden in the database, "plugin"), and for parts the area ("header"/"footer"/"uncategorized"). To edit the site header or footer, find the matching template part here, then get-template + update-template.', |
| 124 | 'category' => 'atarim', |
| 125 | 'input_schema' => [ |
| 126 | 'type' => 'object', |
| 127 | 'properties' => [ |
| 128 | 'type' => [ |
| 129 | 'type' => 'string', |
| 130 | 'enum' => self::TEMPLATE_TYPES, |
| 131 | 'default' => 'wp_template', |
| 132 | 'description' => 'Which set to list. "wp_template" = page templates; "wp_template_part" = header/footer/sidebar regions.', |
| 133 | ], |
| 134 | ], |
| 135 | 'additionalProperties' => false, |
| 136 | ], |
| 137 | 'output_schema' => [ |
| 138 | 'type' => 'object', |
| 139 | 'properties' => [ |
| 140 | 'success' => [ 'type' => 'boolean' ], |
| 141 | 'type' => [ 'type' => 'string' ], |
| 142 | 'total' => [ 'type' => 'integer' ], |
| 143 | 'templates' => [ 'type' => 'array' ], |
| 144 | 'message' => [ 'type' => 'string' ], |
| 145 | ], |
| 146 | 'required' => [ 'success', 'message' ], |
| 147 | ], |
| 148 | 'execute_callback' => function( $input = [] ) { |
| 149 | $type = $this->avcf_resolve_type( $input ); |
| 150 | if ( $type === null ) { |
| 151 | return [ 'success' => false, 'message' => 'type must be "wp_template" or "wp_template_part".' ]; |
| 152 | } |
| 153 | |
| 154 | $templates = []; |
| 155 | foreach ( get_block_templates( [], $type ) as $tpl ) { |
| 156 | $entry = [ |
| 157 | 'id' => (string) $tpl->id, |
| 158 | 'slug' => (string) $tpl->slug, |
| 159 | 'title' => (string) $tpl->title, |
| 160 | 'description' => (string) $tpl->description, |
| 161 | 'source' => (string) $tpl->source, |
| 162 | ]; |
| 163 | if ( $type === 'wp_template_part' ) { |
| 164 | $entry['area'] = isset( $tpl->area ) ? (string) $tpl->area : 'uncategorized'; |
| 165 | } |
| 166 | $templates[] = $entry; |
| 167 | } |
| 168 | |
| 169 | return [ |
| 170 | 'success' => true, |
| 171 | 'type' => $type, |
| 172 | 'total' => count( $templates ), |
| 173 | 'templates' => $templates, |
| 174 | 'message' => sprintf( '%d %s(s) available.', count( $templates ), $type === 'wp_template_part' ? 'template part' : 'template' ), |
| 175 | ]; |
| 176 | }, |
| 177 | 'permission_callback' => function() { |
| 178 | return current_user_can( 'edit_theme_options' ); |
| 179 | }, |
| 180 | 'meta' => [ |
| 181 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 182 | 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ], |
| 183 | ], |
| 184 | ] ); |
| 185 | } |
| 186 | |
| 187 | private function register_get() { |
| 188 | wp_register_ability( 'atarim/get-template', [ |
| 189 | 'label' => 'Get Template', |
| 190 | 'description' => 'Returns one template or template part in full, including its raw Gutenberg block markup (content) and a top-level block summary (the ordered list of block names it is composed of). Identify it by id ("stylesheet//slug" from list-templates) OR by slug + type. is_custom is true when the template has been overridden in the database, false when it is still the pristine theme file. Read this before update-template so you can edit the existing markup rather than replace it blindly.', |
| 191 | 'category' => 'atarim', |
| 192 | 'input_schema' => [ |
| 193 | 'type' => 'object', |
| 194 | 'properties' => [ |
| 195 | 'id' => [ 'type' => 'string', 'description' => 'Full template id "stylesheet//slug". Pass id OR slug+type.' ], |
| 196 | 'slug' => [ 'type' => 'string', 'description' => 'Template slug, e.g. "home" or "header". Requires type.' ], |
| 197 | 'type' => [ 'type' => 'string', 'enum' => self::TEMPLATE_TYPES, 'default' => 'wp_template' ], |
| 198 | ], |
| 199 | 'additionalProperties' => false, |
| 200 | ], |
| 201 | 'output_schema' => [ |
| 202 | 'type' => 'object', |
| 203 | 'properties' => [ |
| 204 | 'success' => [ 'type' => 'boolean' ], |
| 205 | 'template' => [ 'type' => 'object' ], |
| 206 | 'message' => [ 'type' => 'string' ], |
| 207 | ], |
| 208 | 'required' => [ 'success', 'message' ], |
| 209 | ], |
| 210 | 'execute_callback' => function( $input = [] ) { |
| 211 | list( $tpl, $type, $error ) = $this->avcf_locate_template( $input ); |
| 212 | if ( $error !== null ) { |
| 213 | return [ 'success' => false, 'message' => $error ]; |
| 214 | } |
| 215 | |
| 216 | $content = (string) $tpl->content; |
| 217 | $summary = []; |
| 218 | if ( function_exists( 'parse_blocks' ) ) { |
| 219 | foreach ( parse_blocks( $content ) as $block ) { |
| 220 | if ( ! empty( $block['blockName'] ) ) { |
| 221 | $summary[] = (string) $block['blockName']; |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | $template = [ |
| 227 | 'id' => (string) $tpl->id, |
| 228 | 'slug' => (string) $tpl->slug, |
| 229 | 'type' => $type, |
| 230 | 'title' => (string) $tpl->title, |
| 231 | 'description' => (string) $tpl->description, |
| 232 | 'source' => (string) $tpl->source, |
| 233 | 'is_custom' => ( (string) $tpl->source === 'custom' ), |
| 234 | 'content' => $content, |
| 235 | 'block_summary'=> $summary, |
| 236 | ]; |
| 237 | if ( $type === 'wp_template_part' ) { |
| 238 | $template['area'] = isset( $tpl->area ) ? (string) $tpl->area : 'uncategorized'; |
| 239 | } |
| 240 | |
| 241 | return [ |
| 242 | 'success' => true, |
| 243 | 'template' => $template, |
| 244 | 'message' => sprintf( 'Template "%s" (%s) has %d top-level block(s).', $tpl->slug, $tpl->source, count( $summary ) ), |
| 245 | ]; |
| 246 | }, |
| 247 | 'permission_callback' => function() { |
| 248 | return current_user_can( 'edit_theme_options' ); |
| 249 | }, |
| 250 | 'meta' => [ |
| 251 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 252 | 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ], |
| 253 | ], |
| 254 | ] ); |
| 255 | } |
| 256 | |
| 257 | private function register_update() { |
| 258 | wp_register_ability( 'atarim/update-template', [ |
| 259 | 'label' => 'Update Template', |
| 260 | 'description' => 'Creates or replaces a template / template part, overriding the theme file with a database customisation. This is how you edit the site-wide header and footer (type=wp_template_part) or the layout of the home/page/single screens (type=wp_template). Identify by slug + type (id is also accepted). content is the FULL new Gutenberg block markup for the template — it REPLACES the previous content, so pass the complete markup (use get-template first to start from the current markup, or compose from patterns via list-patterns/get-pattern). content_format: "blocks" (default) stores markup as-is; "auto" wraps plain text in paragraph blocks. For a brand-new template part, also pass area ("header"/"footer"/"uncategorized"). Use revert-template to undo. Malformed block markup can visually break the site — validate structure before writing.', |
| 261 | 'category' => 'atarim', |
| 262 | 'input_schema' => [ |
| 263 | 'type' => 'object', |
| 264 | 'properties' => [ |
| 265 | 'slug' => [ 'type' => 'string', 'description' => 'Template slug, e.g. "home" or "header". Pass slug+type OR id.' ], |
| 266 | 'type' => [ 'type' => 'string', 'enum' => self::TEMPLATE_TYPES, 'default' => 'wp_template' ], |
| 267 | 'id' => [ 'type' => 'string', 'description' => 'Full "stylesheet//slug" id (alternative to slug+type).' ], |
| 268 | 'content' => [ 'type' => 'string', 'description' => 'Full replacement Gutenberg block markup.' ], |
| 269 | 'content_format' => [ 'type' => 'string', 'enum' => [ 'blocks', 'auto', 'raw' ], 'default' => 'blocks' ], |
| 270 | 'title' => [ 'type' => 'string', 'description' => 'Optional title. Defaults to the existing/derived title.' ], |
| 271 | 'description' => [ 'type' => 'string', 'description' => 'Optional description (stored on the template).' ], |
| 272 | 'area' => [ 'type' => 'string', 'description' => 'Template parts only: "header", "footer", or "uncategorized". Defaults to the existing area or "uncategorized".' ], |
| 273 | ], |
| 274 | 'required' => [ 'content' ], |
| 275 | 'additionalProperties' => false, |
| 276 | ], |
| 277 | 'output_schema' => [ |
| 278 | 'type' => 'object', |
| 279 | 'properties' => [ |
| 280 | 'success' => [ 'type' => 'boolean' ], |
| 281 | 'id' => [ 'type' => 'string' ], |
| 282 | 'wp_id' => [ 'type' => 'integer' ], |
| 283 | 'created' => [ 'type' => 'boolean' ], |
| 284 | 'message' => [ 'type' => 'string' ], |
| 285 | ], |
| 286 | 'required' => [ 'success', 'message' ], |
| 287 | ], |
| 288 | 'execute_callback' => function( $input = [] ) { |
| 289 | $type = $this->avcf_resolve_type( $input ); |
| 290 | $slug = $this->avcf_resolve_slug( $input ); |
| 291 | if ( $type === null || $slug === '' ) { |
| 292 | return [ 'success' => false, 'message' => 'Provide slug + type, or a full id.' ]; |
| 293 | } |
| 294 | if ( ! isset( $input['content'] ) || ! is_string( $input['content'] ) || $input['content'] === '' ) { |
| 295 | return [ 'success' => false, 'message' => 'content is required.' ]; |
| 296 | } |
| 297 | |
| 298 | $format = isset( $input['content_format'] ) ? (string) $input['content_format'] : 'blocks'; |
| 299 | $content = $this->avcf_prepare_content_body( (string) $input['content'], $format ); |
| 300 | $area = isset( $input['area'] ) ? sanitize_key( (string) $input['area'] ) : null; |
| 301 | $title = isset( $input['title'] ) ? sanitize_text_field( (string) $input['title'] ) : null; |
| 302 | $desc = isset( $input['description'] ) ? sanitize_text_field( (string) $input['description'] ) : null; |
| 303 | |
| 304 | list( $wp_id, $created, $err ) = $this->avcf_upsert_template( $type, $slug, $content, $title, $desc, $area ); |
| 305 | if ( $err !== null ) { |
| 306 | return [ 'success' => false, 'message' => $err ]; |
| 307 | } |
| 308 | |
| 309 | return [ |
| 310 | 'success' => true, |
| 311 | 'id' => get_stylesheet() . '//' . $slug, |
| 312 | 'wp_id' => (int) $wp_id, |
| 313 | 'created' => (bool) $created, |
| 314 | 'message' => sprintf( '%s "%s" %s.', $type === 'wp_template_part' ? 'Template part' : 'Template', $slug, $created ? 'created' : 'updated' ), |
| 315 | ]; |
| 316 | }, |
| 317 | 'permission_callback' => function() { |
| 318 | return current_user_can( 'edit_theme_options' ); |
| 319 | }, |
| 320 | 'meta' => [ |
| 321 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 322 | 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => true ], |
| 323 | ], |
| 324 | ] ); |
| 325 | } |
| 326 | |
| 327 | private function register_revert() { |
| 328 | wp_register_ability( 'atarim/revert-template', [ |
| 329 | 'label' => 'Revert Template', |
| 330 | 'description' => 'Removes a template / template-part customisation and restores the original theme file. Identify by slug + type or id. Only affects templates whose source is "custom"; a template still served from the theme file is left untouched and reported as such. This deletes the overriding database row (irreversible), it does not delete the theme file. Use this to undo an update-template that went wrong.', |
| 331 | 'category' => 'atarim', |
| 332 | 'input_schema' => [ |
| 333 | 'type' => 'object', |
| 334 | 'properties' => [ |
| 335 | 'slug' => [ 'type' => 'string' ], |
| 336 | 'type' => [ 'type' => 'string', 'enum' => self::TEMPLATE_TYPES, 'default' => 'wp_template' ], |
| 337 | 'id' => [ 'type' => 'string' ], |
| 338 | ], |
| 339 | 'additionalProperties' => false, |
| 340 | ], |
| 341 | 'output_schema' => [ |
| 342 | 'type' => 'object', |
| 343 | 'properties' => [ |
| 344 | 'success' => [ 'type' => 'boolean' ], |
| 345 | 'reverted' => [ 'type' => 'boolean' ], |
| 346 | 'source' => [ 'type' => 'string' ], |
| 347 | 'message' => [ 'type' => 'string' ], |
| 348 | ], |
| 349 | 'required' => [ 'success', 'message' ], |
| 350 | ], |
| 351 | 'execute_callback' => function( $input = [] ) { |
| 352 | list( $tpl, $type, $error ) = $this->avcf_locate_template( $input ); |
| 353 | if ( $error !== null ) { |
| 354 | return [ 'success' => false, 'message' => $error ]; |
| 355 | } |
| 356 | if ( (string) $tpl->source !== 'custom' || empty( $tpl->wp_id ) ) { |
| 357 | return [ |
| 358 | 'success' => true, |
| 359 | 'reverted' => false, |
| 360 | 'source' => (string) $tpl->source, |
| 361 | 'message' => sprintf( 'Template "%s" is served from the theme file (source: %s) — nothing to revert.', $tpl->slug, $tpl->source ), |
| 362 | ]; |
| 363 | } |
| 364 | |
| 365 | $deleted = wp_delete_post( (int) $tpl->wp_id, true ); |
| 366 | if ( ! $deleted ) { |
| 367 | return [ 'success' => false, 'reverted' => false, 'message' => sprintf( 'Failed to delete customisation for "%s".', $tpl->slug ) ]; |
| 368 | } |
| 369 | |
| 370 | return [ |
| 371 | 'success' => true, |
| 372 | 'reverted' => true, |
| 373 | 'source' => 'theme', |
| 374 | 'message' => sprintf( 'Template "%s" reverted to the theme file.', $tpl->slug ), |
| 375 | ]; |
| 376 | }, |
| 377 | 'permission_callback' => function() { |
| 378 | return current_user_can( 'edit_theme_options' ); |
| 379 | }, |
| 380 | 'meta' => [ |
| 381 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 382 | 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => true ], |
| 383 | ], |
| 384 | ] ); |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * Resolve the template type from input, defaulting to wp_template. |
| 389 | * |
| 390 | * @param array $input |
| 391 | * @return string|null Null when an explicit invalid type was passed. |
| 392 | */ |
| 393 | private function avcf_resolve_type( $input ) { |
| 394 | if ( ! empty( $input['id'] ) && empty( $input['type'] ) ) { |
| 395 | return 'wp_template'; |
| 396 | } |
| 397 | $type = isset( $input['type'] ) ? (string) $input['type'] : 'wp_template'; |
| 398 | return in_array( $type, self::TEMPLATE_TYPES, true ) ? $type : null; |
| 399 | } |
| 400 | |
| 401 | /** |
| 402 | * Resolve the template slug from either a full id or an explicit slug. |
| 403 | * |
| 404 | * @param array $input |
| 405 | * @return string |
| 406 | */ |
| 407 | private function avcf_resolve_slug( $input ) { |
| 408 | if ( ! empty( $input['id'] ) && strpos( (string) $input['id'], '//' ) !== false ) { |
| 409 | $parts = explode( '//', (string) $input['id'], 2 ); |
| 410 | return sanitize_title( $parts[1] ); |
| 411 | } |
| 412 | return isset( $input['slug'] ) ? sanitize_title( (string) $input['slug'] ) : ''; |
| 413 | } |
| 414 | |
| 415 | /** |
| 416 | * Locate a template object from input (id, or slug + type). |
| 417 | * |
| 418 | * @param array $input |
| 419 | * @return array{0:?object,1:string,2:?string} [template, type, error] |
| 420 | */ |
| 421 | private function avcf_locate_template( $input ) { |
| 422 | $type = $this->avcf_resolve_type( $input ); |
| 423 | if ( $type === null ) { |
| 424 | return [ null, '', 'type must be "wp_template" or "wp_template_part".' ]; |
| 425 | } |
| 426 | $slug = $this->avcf_resolve_slug( $input ); |
| 427 | if ( $slug === '' ) { |
| 428 | return [ null, $type, 'Provide slug + type, or a full id.' ]; |
| 429 | } |
| 430 | $tpl = get_block_template( get_stylesheet() . '//' . $slug, $type ); |
| 431 | if ( ! $tpl ) { |
| 432 | return [ null, $type, sprintf( 'No %s found with slug "%s".', $type, $slug ) ]; |
| 433 | } |
| 434 | return [ $tpl, $type, null ]; |
| 435 | } |
| 436 | |
| 437 | /** |
| 438 | * Insert or update the database override for a block template. |
| 439 | * |
| 440 | * @param string $type |
| 441 | * @param string $slug |
| 442 | * @param string $content |
| 443 | * @param string|null $title |
| 444 | * @param string|null $description |
| 445 | * @param string|null $area |
| 446 | * @return array{0:int,1:bool,2:?string} [wp_id, created, error] |
| 447 | */ |
| 448 | private function avcf_upsert_template( $type, $slug, $content, $title, $description, $area ) { |
| 449 | $stylesheet = get_stylesheet(); |
| 450 | $existing = get_block_template( $stylesheet . '//' . $slug, $type ); |
| 451 | |
| 452 | $postarr = [ |
| 453 | 'post_type' => $type, |
| 454 | 'post_name' => $slug, |
| 455 | 'post_status' => 'publish', |
| 456 | 'post_content' => $content, |
| 457 | 'post_title' => $title !== null && $title !== '' ? $title : ( $existing ? (string) $existing->title : $slug ), |
| 458 | ]; |
| 459 | if ( $description !== null ) { |
| 460 | $postarr['post_excerpt'] = $description; |
| 461 | } |
| 462 | |
| 463 | $is_update = $existing && ! empty( $existing->wp_id ); |
| 464 | if ( $is_update ) { |
| 465 | $postarr['ID'] = (int) $existing->wp_id; |
| 466 | $wp_id = wp_update_post( $postarr, true ); |
| 467 | } else { |
| 468 | $wp_id = wp_insert_post( $postarr, true ); |
| 469 | } |
| 470 | |
| 471 | if ( is_wp_error( $wp_id ) ) { |
| 472 | return [ 0, false, 'Save failed: ' . $wp_id->get_error_message() ]; |
| 473 | } |
| 474 | |
| 475 | wp_set_object_terms( (int) $wp_id, $stylesheet, 'wp_theme' ); |
| 476 | if ( $type === 'wp_template_part' ) { |
| 477 | $use_area = $area ?: ( $existing && ! empty( $existing->area ) ? (string) $existing->area : 'uncategorized' ); |
| 478 | wp_set_object_terms( (int) $wp_id, $use_area, 'wp_template_part_area' ); |
| 479 | } |
| 480 | |
| 481 | return [ (int) $wp_id, ! $is_update, null ]; |
| 482 | } |
| 483 | |
| 484 | /** |
| 485 | * Collect the slugs of all templates of a given type. |
| 486 | * |
| 487 | * @param string $type |
| 488 | * @return array<int,string> |
| 489 | */ |
| 490 | private function avcf_template_slugs( $type ) { |
| 491 | $slugs = []; |
| 492 | foreach ( get_block_templates( [], $type ) as $tpl ) { |
| 493 | $slugs[] = (string) $tpl->slug; |
| 494 | } |
| 495 | sort( $slugs ); |
| 496 | return $slugs; |
| 497 | } |
| 498 | |
| 499 | /** |
| 500 | * Whether user global styles carry any real customisation beyond the |
| 501 | * version marker and the isGlobalStylesUserThemeJSON flag. |
| 502 | * |
| 503 | * @return bool |
| 504 | */ |
| 505 | private function avcf_global_styles_customised() { |
| 506 | if ( ! class_exists( 'WP_Theme_JSON_Resolver' ) ) { |
| 507 | return false; |
| 508 | } |
| 509 | $gid = WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); |
| 510 | if ( ! $gid ) { |
| 511 | return false; |
| 512 | } |
| 513 | $data = json_decode( (string) get_post( $gid )->post_content, true ); |
| 514 | if ( ! is_array( $data ) ) { |
| 515 | return false; |
| 516 | } |
| 517 | return ! empty( $data['styles'] ) || ! empty( $data['settings'] ); |
| 518 | } |
| 519 | } |
| 520 |