PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.1
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.1
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / doit / abilities / class-avcf-abilities-templates.php
atarim-visual-collaboration / doit / abilities Last commit date
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