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-navigation.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-navigation.php
1072 lines
1 <?php
2 /**
3 * Navigation and site structure MCP abilities.
4 *
5 * Covers classic WordPress menus (nav_menu taxonomy + nav_menu_item posts)
6 * and widget areas (classic and block-based widgets). Block-theme
7 * navigation (the wp_navigation post type used by the Navigation block) is
8 * out of scope here — it is handled by AVCF_Abilities_Block_Navigation
9 * (atarim/list-navigation, get-navigation, update-navigation).
10 *
11 * Per-widget mode detection: each widget instance carries its own type
12 * (classic / block) in reads, and the same type on writes — same pattern
13 * as the type-hint approach in the metadata cluster.
14 *
15 * Menu item writes are REPLACE-mode: the AI sends the full new item set
16 * including a parent_index for each item to express the tree. Order is
17 * the array index. Per-item meta (mega-menu plugin settings, etc.) is
18 * preserved when the AI passes it back on the write.
19 *
20 * Exposed abilities:
21 * atarim/list-menus All menus + locations + counts.
22 * atarim/get-menu One menu with full item tree + per-item meta.
23 * atarim/create-menu New menu; optional location assignment.
24 * atarim/update-menu Rename + reassign location.
25 * atarim/delete-menu Permanent delete (no trash).
26 * atarim/update-menu-items REPLACE menu items with array-index ordering and parent_index tree.
27 * atarim/list-menu-locations Theme-registered locations + currently assigned menu.
28 * atarim/list-widget-areas Sidebars + per-widget type detection.
29 * atarim/update-widget-area REPLACE widgets in a sidebar (classic and block items heterogeneous).
30 * atarim/get-widget-types Available widget type registry.
31 *
32 * @package atarim-visual-collaboration
33 */
34
35 if ( ! defined('ABSPATH') ) {
36 exit;
37 }
38
39 class AVCF_Abilities_Navigation extends AVCF_Abilities_Base {
40
41 public function register() {
42
43 // ---- list-menus ----
44 wp_register_ability( 'atarim/list-menus', [
45 'label' => 'List Navigation Menus',
46 'description' => 'Returns all classic WordPress navigation menus with their item counts and the theme locations each is assigned to. If the active theme is a block theme, returns a block_theme: true flag with a note — block themes typically use wp_navigation posts and the Site Editor for primary navigation, which is out of scope for these abilities. Block themes that ALSO register classic menu locations work normally for those locations.',
47 'category' => 'atarim',
48 'input_schema' => [
49 'type' => 'object',
50 'properties' => [],
51 'additionalProperties' => false,
52 ],
53 'output_schema' => [
54 'type' => 'object',
55 'properties' => [
56 'success' => [ 'type' => 'boolean' ],
57 'total' => [ 'type' => 'integer' ],
58 'menus' => [ 'type' => 'array' ],
59 'block_theme' => [ 'type' => 'boolean' ],
60 'block_theme_note' => [ 'type' => 'string' ],
61 'message' => [ 'type' => 'string' ],
62 ],
63 'required' => [ 'success', 'message' ],
64 ],
65 'execute_callback' => function( $input = [] ) {
66 $menus_raw = wp_get_nav_menus();
67 $locations = (array) get_nav_menu_locations();
68
69 $menus = [];
70 foreach ( $menus_raw as $menu ) {
71 $assigned_to = [];
72 foreach ( $locations as $location => $menu_id ) {
73 if ( (int) $menu_id === (int) $menu->term_id ) {
74 $assigned_to[] = (string) $location;
75 }
76 }
77 $menus[] = [
78 'id' => (int) $menu->term_id,
79 'name' => (string) $menu->name,
80 'slug' => (string) $menu->slug,
81 'description' => (string) $menu->description,
82 'item_count' => (int) $menu->count,
83 'locations' => $assigned_to,
84 ];
85 }
86
87 $is_block_theme = function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
88 $note = '';
89 if ( $is_block_theme ) {
90 $note = 'Active theme is a block theme. Classic menus here may still be used by widgets, footer, or legacy locations, but primary navigation in block themes typically uses wp_navigation posts edited via the Site Editor — those are out of scope for these abilities.';
91 }
92
93 return [
94 'success' => true,
95 'total' => count( $menus ),
96 'menus' => $menus,
97 'block_theme' => $is_block_theme,
98 'block_theme_note' => $note,
99 'message' => sprintf( '%d classic menu(s) on the site.', count( $menus ) ),
100 ];
101 },
102 'permission_callback' => function() {
103 return current_user_can( 'edit_theme_options' );
104 },
105 'meta' => [
106 'mcp' => [ 'public' => true, 'type' => 'tool' ],
107 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
108 ],
109 ] );
110
111 // ---- get-menu ----
112 wp_register_ability( 'atarim/get-menu', [
113 'label' => 'Get Menu',
114 'description' => 'Returns full detail for one menu including the ordered item tree. Each item has: type (custom/post_type/taxonomy), object (post type slug or taxonomy slug for non-custom), object_id, url, label, target, css_classes, description, xfn, menu_item_id, parent_item_id (for the tree), order, and meta (an object of all non-WordPress-internal meta keys on the item, useful for preserving mega-menu plugin settings on round-trips). Pass through items[].meta unchanged in update-menu-items to preserve third-party plugin settings.',
115 'category' => 'atarim',
116 'input_schema' => [
117 'type' => 'object',
118 'properties' => [
119 'id' => [ 'type' => 'integer', 'minimum' => 1, 'description' => 'Menu term ID. Pass id OR slug.' ],
120 'slug' => [ 'type' => 'string', 'description' => 'Menu slug.' ],
121 ],
122 'additionalProperties' => false,
123 ],
124 'output_schema' => [
125 'type' => 'object',
126 'properties' => [
127 'success' => [ 'type' => 'boolean' ],
128 'menu' => [ 'type' => 'object' ],
129 'message' => [ 'type' => 'string' ],
130 ],
131 'required' => [ 'success', 'message' ],
132 ],
133 'execute_callback' => function( $input = [] ) {
134 $id = isset( $input['id'] ) ? (int) $input['id'] : 0;
135 $slug = isset( $input['slug'] ) ? (string) $input['slug'] : '';
136
137 if ( $id <= 0 && $slug === '' ) {
138 return [ 'success' => false, 'message' => 'Pass id or slug.' ];
139 }
140 $menu = $id > 0 ? wp_get_nav_menu_object( $id ) : wp_get_nav_menu_object( $slug );
141 if ( ! $menu ) {
142 return [ 'success' => false, 'message' => 'Menu not found.' ];
143 }
144
145 $items_raw = wp_get_nav_menu_items( $menu->term_id, [ 'update_post_term_cache' => false ] );
146 if ( ! is_array( $items_raw ) ) {
147 $items_raw = [];
148 }
149
150 $items = [];
151 foreach ( $items_raw as $item ) {
152 // Pull all non-WordPress-internal meta for round-trip preservation.
153 $all_meta = get_post_meta( $item->ID );
154 $clean_meta = [];
155 // WordPress-internal nav menu meta keys we manage ourselves on write.
156 $internal_keys = [
157 '_menu_item_type', '_menu_item_menu_item_parent', '_menu_item_object_id',
158 '_menu_item_object', '_menu_item_target', '_menu_item_classes',
159 '_menu_item_xfn', '_menu_item_url', '_menu_item_orphaned',
160 ];
161 foreach ( $all_meta as $k => $v ) {
162 if ( in_array( $k, $internal_keys, true ) ) continue;
163 // get_post_meta($id) returns arrays; unwrap single values.
164 $clean_meta[ $k ] = ( is_array( $v ) && count( $v ) === 1 ) ? $v[0] : $v;
165 }
166
167 $items[] = [
168 'menu_item_id' => (int) $item->ID,
169 'parent_item_id' => (int) $item->menu_item_parent,
170 'order' => (int) $item->menu_order,
171 'type' => (string) $item->type,
172 'object' => (string) $item->object,
173 'object_id' => (int) $item->object_id,
174 'url' => (string) $item->url,
175 'label' => (string) $item->title,
176 'target' => (string) $item->target,
177 'css_classes' => array_values( array_filter( (array) $item->classes ) ),
178 'description' => (string) $item->description,
179 'xfn' => (string) $item->xfn,
180 'meta' => $clean_meta,
181 ];
182 }
183
184 // Sort by order (wp_get_nav_menu_items returns this way by default but be defensive).
185 usort( $items, function( $a, $b ) {
186 return ( $a['order'] <=> $b['order'] );
187 } );
188
189 $locations = (array) get_nav_menu_locations();
190 $assigned_to = [];
191 foreach ( $locations as $loc => $mid ) {
192 if ( (int) $mid === (int) $menu->term_id ) {
193 $assigned_to[] = (string) $loc;
194 }
195 }
196
197 return [
198 'success' => true,
199 'menu' => [
200 'id' => (int) $menu->term_id,
201 'name' => (string) $menu->name,
202 'slug' => (string) $menu->slug,
203 'description' => (string) $menu->description,
204 'locations' => $assigned_to,
205 'item_count' => count( $items ),
206 'items' => $items,
207 ],
208 'message' => sprintf( 'Menu "%s" has %d item(s).', $menu->name, count( $items ) ),
209 ];
210 },
211 'permission_callback' => function() {
212 return current_user_can( 'edit_theme_options' );
213 },
214 'meta' => [
215 'mcp' => [ 'public' => true, 'type' => 'tool' ],
216 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
217 ],
218 ] );
219
220 // ---- create-menu ----
221 wp_register_ability( 'atarim/create-menu', [
222 'label' => 'Create Menu',
223 'description' => 'Create a new classic menu by name. Optional assign_locations: array of theme location names to attach the new menu to. Use list-menu-locations first to know what locations are available. Returns the new menu ID.',
224 'category' => 'atarim',
225 'input_schema' => [
226 'type' => 'object',
227 'properties' => [
228 'name' => [ 'type' => 'string', 'minLength' => 1 ],
229 'assign_locations' => [
230 'type' => 'array',
231 'description' => 'Optional list of theme location slugs to assign this new menu to.',
232 'items' => [ 'type' => 'string' ],
233 ],
234 ],
235 'required' => [ 'name' ],
236 'additionalProperties' => false,
237 ],
238 'output_schema' => [
239 'type' => 'object',
240 'properties' => [
241 'success' => [ 'type' => 'boolean' ],
242 'id' => [ 'type' => 'integer' ],
243 'name' => [ 'type' => 'string' ],
244 'assigned_locations'=> [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
245 'skipped_locations' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
246 'message' => [ 'type' => 'string' ],
247 ],
248 'required' => [ 'success', 'message' ],
249 ],
250 'execute_callback' => function( $input = [] ) {
251 $name = isset( $input['name'] ) ? sanitize_text_field( (string) $input['name'] ) : '';
252 if ( $name === '' ) {
253 return [ 'success' => false, 'message' => 'name is required.' ];
254 }
255 if ( wp_get_nav_menu_object( $name ) ) {
256 return [ 'success' => false, 'message' => sprintf( 'Menu "%s" already exists.', $name ) ];
257 }
258
259 $id = wp_create_nav_menu( $name );
260 if ( is_wp_error( $id ) ) {
261 return [ 'success' => false, 'message' => 'Create failed: ' . $id->get_error_message() ];
262 }
263
264 $assigned = [];
265 $skipped = [];
266 if ( ! empty( $input['assign_locations'] ) && is_array( $input['assign_locations'] ) ) {
267 $registered = (array) get_registered_nav_menus();
268 $current = (array) get_nav_menu_locations();
269 foreach ( $input['assign_locations'] as $loc ) {
270 $loc = sanitize_key( (string) $loc );
271 if ( $loc === '' ) continue;
272 if ( ! isset( $registered[ $loc ] ) ) {
273 $skipped[] = $loc;
274 continue;
275 }
276 $current[ $loc ] = (int) $id;
277 $assigned[] = $loc;
278 }
279 if ( ! empty( $assigned ) ) {
280 set_theme_mod( 'nav_menu_locations', $current );
281 }
282 }
283
284 return [
285 'success' => true,
286 'id' => (int) $id,
287 'name' => $name,
288 'assigned_locations' => $assigned,
289 'skipped_locations' => $skipped,
290 'message' => empty( $skipped )
291 ? sprintf( 'Menu "%s" created (ID %d).', $name, $id )
292 : sprintf( 'Menu "%s" created (ID %d). Locations not registered by theme were skipped: %s.', $name, $id, implode( ', ', $skipped ) ),
293 ];
294 },
295 'permission_callback' => function() {
296 return current_user_can( 'edit_theme_options' );
297 },
298 'meta' => [
299 'mcp' => [ 'public' => true, 'type' => 'tool' ],
300 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
301 ],
302 ] );
303
304 // ---- update-menu ----
305 wp_register_ability( 'atarim/update-menu', [
306 'label' => 'Update Menu',
307 'description' => 'Rename a menu and/or reassign its location set. assign_locations REPLACES the menu\'s location assignments — pass empty array to remove from all locations, or omit the field to leave assignments untouched. Note: assigning this menu to a location that already has another menu assigned WILL move the assignment to this menu (last-write-wins).',
308 'category' => 'atarim',
309 'input_schema' => [
310 'type' => 'object',
311 'properties' => [
312 'id' => [ 'type' => 'integer', 'minimum' => 1 ],
313 'name' => [ 'type' => 'string', 'description' => 'New menu name. Omit to leave unchanged.' ],
314 'description' => [ 'type' => 'string' ],
315 'assign_locations' => [
316 'type' => 'array',
317 'description' => 'REPLACE the menu\'s assigned location set. Omit to leave assignments untouched. Empty array unassigns from all locations.',
318 'items' => [ 'type' => 'string' ],
319 ],
320 ],
321 'required' => [ 'id' ],
322 'additionalProperties' => false,
323 ],
324 'output_schema' => [
325 'type' => 'object',
326 'properties' => [
327 'success' => [ 'type' => 'boolean' ],
328 'id' => [ 'type' => 'integer' ],
329 'updated' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
330 'assigned_locations'=> [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
331 'skipped_locations' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
332 'message' => [ 'type' => 'string' ],
333 ],
334 'required' => [ 'success', 'message' ],
335 ],
336 'execute_callback' => function( $input = [] ) {
337 $id = isset( $input['id'] ) ? (int) $input['id'] : 0;
338 if ( $id <= 0 ) {
339 return [ 'success' => false, 'message' => 'id is required.' ];
340 }
341 $menu = wp_get_nav_menu_object( $id );
342 if ( ! $menu ) {
343 return [ 'success' => false, 'message' => sprintf( 'Menu %d not found.', $id ) ];
344 }
345
346 $updated = [];
347 $args = [];
348 if ( array_key_exists( 'name', $input ) && $input['name'] !== '' ) {
349 $args['menu-name'] = sanitize_text_field( (string) $input['name'] );
350 $updated[] = 'name';
351 }
352 if ( array_key_exists( 'description', $input ) ) {
353 $args['description'] = sanitize_text_field( (string) $input['description'] );
354 $updated[] = 'description';
355 }
356
357 if ( ! empty( $args ) ) {
358 $result = wp_update_nav_menu_object( $id, $args );
359 if ( is_wp_error( $result ) ) {
360 return [ 'success' => false, 'id' => $id, 'updated' => $updated, 'message' => 'Update failed: ' . $result->get_error_message() ];
361 }
362 }
363
364 $assigned = [];
365 $skipped = [];
366 if ( array_key_exists( 'assign_locations', $input ) && is_array( $input['assign_locations'] ) ) {
367 $registered = (array) get_registered_nav_menus();
368 $current = (array) get_nav_menu_locations();
369
370 // Remove this menu from any locations it's currently in.
371 foreach ( $current as $loc => $mid ) {
372 if ( (int) $mid === $id ) {
373 unset( $current[ $loc ] );
374 }
375 }
376
377 foreach ( $input['assign_locations'] as $loc ) {
378 $loc = sanitize_key( (string) $loc );
379 if ( $loc === '' ) continue;
380 if ( ! isset( $registered[ $loc ] ) ) {
381 $skipped[] = $loc;
382 continue;
383 }
384 $current[ $loc ] = $id;
385 $assigned[] = $loc;
386 }
387 set_theme_mod( 'nav_menu_locations', $current );
388 $updated[] = 'assign_locations';
389 }
390
391 if ( empty( $updated ) ) {
392 return [ 'success' => false, 'id' => $id, 'updated' => [], 'message' => 'No fields provided to update.' ];
393 }
394
395 return [
396 'success' => true,
397 'id' => $id,
398 'updated' => $updated,
399 'assigned_locations' => $assigned,
400 'skipped_locations' => $skipped,
401 'message' => sprintf( 'Menu %d updated: %s.', $id, implode( ', ', $updated ) ),
402 ];
403 },
404 'permission_callback' => function() {
405 return current_user_can( 'edit_theme_options' );
406 },
407 'meta' => [
408 'mcp' => [ 'public' => true, 'type' => 'tool' ],
409 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
410 ],
411 ] );
412
413 // ---- delete-menu ----
414 wp_register_ability( 'atarim/delete-menu', [
415 'label' => 'Delete Menu',
416 'description' => 'Permanently delete a menu and all its items. WordPress\'s wp_delete_nav_menu does NOT trash — this is irreversible. Any location assignments for this menu are dropped automatically. To soft-delete, unassign from all locations via update-menu instead.',
417 'category' => 'atarim',
418 'input_schema' => [
419 'type' => 'object',
420 'properties' => [
421 'id' => [ 'type' => 'integer', 'minimum' => 1 ],
422 ],
423 'required' => [ 'id' ],
424 'additionalProperties' => false,
425 ],
426 'output_schema' => [
427 'type' => 'object',
428 'properties' => [
429 'success' => [ 'type' => 'boolean' ],
430 'id' => [ 'type' => 'integer' ],
431 'message' => [ 'type' => 'string' ],
432 ],
433 'required' => [ 'success', 'message' ],
434 ],
435 'execute_callback' => function( $input = [] ) {
436 $id = isset( $input['id'] ) ? (int) $input['id'] : 0;
437 if ( $id <= 0 ) {
438 return [ 'success' => false, 'message' => 'id is required.' ];
439 }
440 $menu = wp_get_nav_menu_object( $id );
441 if ( ! $menu ) {
442 return [ 'success' => false, 'id' => $id, 'message' => sprintf( 'Menu %d not found.', $id ) ];
443 }
444 $name = $menu->name;
445 $result = wp_delete_nav_menu( $id );
446 if ( is_wp_error( $result ) ) {
447 return [ 'success' => false, 'id' => $id, 'message' => 'Delete failed: ' . $result->get_error_message() ];
448 }
449 if ( $result === false ) {
450 return [ 'success' => false, 'id' => $id, 'message' => 'Delete failed.' ];
451 }
452 return [
453 'success' => true,
454 'id' => $id,
455 'message' => sprintf( 'Menu "%s" (ID %d) permanently deleted.', $name, $id ),
456 ];
457 },
458 'permission_callback' => function() {
459 return current_user_can( 'edit_theme_options' );
460 },
461 'meta' => [
462 'mcp' => [ 'public' => true, 'type' => 'tool' ],
463 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => false ],
464 ],
465 ] );
466
467 // ---- update-menu-items ----
468 wp_register_ability( 'atarim/update-menu-items', [
469 'label' => 'Update Menu Items',
470 'description' => 'REPLACE all items in a menu. Send the full new tree as an array of items; order is the array index. Each item has: type (custom/post_type/taxonomy), object (for non-custom: post type or taxonomy slug), object_id (for non-custom: the referenced ID), url (for custom only), label, target (_blank or empty), css_classes, description, xfn, parent_index (index into THIS array, or null for top-level — must reference an EARLIER index, no forward refs), meta (object of plugin-specific meta keys to preserve, e.g. mega-menu settings from get-menu). Existing items not in the new list are deleted. Per-item meta is preserved when passed back unchanged.',
471 'category' => 'atarim',
472 'input_schema' => [
473 'type' => 'object',
474 'properties' => [
475 'menu_id' => [ 'type' => 'integer', 'minimum' => 1 ],
476 'items' => [
477 'type' => 'array',
478 'description' => 'New ordered list of menu items. Empty array clears the menu.',
479 'items' => [
480 'type' => 'object',
481 'properties' => [
482 'type' => [ 'type' => 'string', 'enum' => [ 'custom', 'post_type', 'taxonomy' ] ],
483 'object' => [ 'type' => 'string', 'description' => 'For type=post_type: the post type slug. For type=taxonomy: the taxonomy slug. Required for non-custom types.' ],
484 'object_id' => [ 'type' => 'integer', 'description' => 'Referenced post ID or term ID. Required for non-custom types.' ],
485 'url' => [ 'type' => 'string', 'description' => 'Only used when type=custom.' ],
486 'label' => [ 'type' => 'string' ],
487 'target' => [ 'type' => 'string', 'enum' => [ '', '_blank' ] ],
488 'css_classes' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
489 'description' => [ 'type' => 'string' ],
490 'xfn' => [ 'type' => 'string' ],
491 'parent_index'=> [ 'type' => [ 'integer', 'null' ], 'description' => 'Zero-based index into this items array, or null for top-level. Must point to an EARLIER index than this one.' ],
492 'meta' => [ 'type' => 'object', 'description' => 'Optional. Per-item meta from the get-menu response, passed back to preserve third-party plugin settings.' ],
493 ],
494 'required' => [ 'type', 'label' ],
495 ],
496 ],
497 ],
498 'required' => [ 'menu_id', 'items' ],
499 'additionalProperties' => false,
500 ],
501 'output_schema' => [
502 'type' => 'object',
503 'properties' => [
504 'success' => [ 'type' => 'boolean' ],
505 'menu_id' => [ 'type' => 'integer' ],
506 'created' => [ 'type' => 'integer' ],
507 'deleted' => [ 'type' => 'integer' ],
508 'warnings'=> [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
509 'message' => [ 'type' => 'string' ],
510 ],
511 'required' => [ 'success', 'message' ],
512 ],
513 'execute_callback' => function( $input = [] ) {
514 $menu_id = isset( $input['menu_id'] ) ? (int) $input['menu_id'] : 0;
515 if ( $menu_id <= 0 ) {
516 return [ 'success' => false, 'message' => 'menu_id is required.' ];
517 }
518 $menu = wp_get_nav_menu_object( $menu_id );
519 if ( ! $menu ) {
520 return [ 'success' => false, 'message' => sprintf( 'Menu %d not found.', $menu_id ) ];
521 }
522
523 $items_in = isset( $input['items'] ) && is_array( $input['items'] ) ? $input['items'] : null;
524 if ( $items_in === null ) {
525 return [ 'success' => false, 'message' => 'items is required (pass empty array to clear the menu).' ];
526 }
527
528 $warnings = [];
529
530 // 1. Validate parent_index references — must point to earlier index or be null.
531 $validated = [];
532 foreach ( $items_in as $idx => $item ) {
533 if ( ! is_array( $item ) ) {
534 $warnings[] = sprintf( 'Item at index %d is not an object — skipped.', $idx );
535 continue;
536 }
537 if ( empty( $item['type'] ) || empty( $item['label'] ) ) {
538 $warnings[] = sprintf( 'Item at index %d is missing type or label — skipped.', $idx );
539 continue;
540 }
541 if ( ! in_array( $item['type'], [ 'custom', 'post_type', 'taxonomy' ], true ) ) {
542 $warnings[] = sprintf( 'Item at index %d has unknown type "%s" — skipped.', $idx, $item['type'] );
543 continue;
544 }
545 if ( $item['type'] !== 'custom' ) {
546 if ( empty( $item['object'] ) || empty( $item['object_id'] ) ) {
547 $warnings[] = sprintf( 'Item at index %d is type=%s but missing object or object_id — skipped.', $idx, $item['type'] );
548 continue;
549 }
550 // Validate reference exists.
551 if ( $item['type'] === 'post_type' ) {
552 $referenced = get_post( (int) $item['object_id'] );
553 if ( ! $referenced || $referenced->post_type !== (string) $item['object'] ) {
554 $warnings[] = sprintf( 'Item at index %d references %s ID %d but it does not exist or is not of type "%s" — skipped.', $idx, $item['object'], $item['object_id'], $item['object'] );
555 continue;
556 }
557 } elseif ( $item['type'] === 'taxonomy' ) {
558 $referenced = get_term( (int) $item['object_id'], (string) $item['object'] );
559 if ( ! $referenced || is_wp_error( $referenced ) ) {
560 $warnings[] = sprintf( 'Item at index %d references %s term %d but it does not exist — skipped.', $idx, $item['object'], $item['object_id'] );
561 continue;
562 }
563 }
564 }
565
566 // Validate parent_index points to an EARLIER index that wasn't itself skipped.
567 $parent_idx = array_key_exists( 'parent_index', $item ) ? $item['parent_index'] : null;
568 if ( $parent_idx !== null ) {
569 $parent_idx = (int) $parent_idx;
570 if ( $parent_idx < 0 || $parent_idx >= $idx ) {
571 $warnings[] = sprintf( 'Item at index %d has invalid parent_index %d (must reference an earlier index) — treated as top-level.', $idx, $parent_idx );
572 $parent_idx = null;
573 }
574 }
575
576 $validated[ $idx ] = [ 'item' => $item, 'parent_index' => $parent_idx ];
577 }
578
579 // 2. Delete existing items.
580 $existing = wp_get_nav_menu_items( $menu_id );
581 $deleted_count = 0;
582 if ( is_array( $existing ) ) {
583 foreach ( $existing as $existing_item ) {
584 wp_delete_post( (int) $existing_item->ID, true );
585 $deleted_count++;
586 }
587 }
588
589 // 3. Create new items in order, building index -> new_post_id map for parent resolution.
590 $index_to_new_id = [];
591 $created_count = 0;
592
593 foreach ( $validated as $idx => $entry ) {
594 $item = $entry['item'];
595 $parent_idx = $entry['parent_index'];
596 $parent_post_id = ( $parent_idx !== null && isset( $index_to_new_id[ $parent_idx ] ) )
597 ? $index_to_new_id[ $parent_idx ]
598 : 0;
599
600 $item_data = [
601 'menu-item-type' => (string) $item['type'],
602 'menu-item-title' => sanitize_text_field( (string) $item['label'] ),
603 'menu-item-status' => 'publish',
604 'menu-item-parent-id' => (int) $parent_post_id,
605 'menu-item-target' => isset( $item['target'] ) && $item['target'] === '_blank' ? '_blank' : '',
606 'menu-item-classes' => isset( $item['css_classes'] ) && is_array( $item['css_classes'] )
607 ? implode( ' ', array_map( 'sanitize_html_class', $item['css_classes'] ) )
608 : '',
609 'menu-item-description' => isset( $item['description'] ) ? sanitize_text_field( (string) $item['description'] ) : '',
610 'menu-item-xfn' => isset( $item['xfn'] ) ? sanitize_text_field( (string) $item['xfn'] ) : '',
611 ];
612
613 if ( $item['type'] === 'custom' ) {
614 $item_data['menu-item-url'] = isset( $item['url'] ) ? esc_url_raw( (string) $item['url'] ) : '';
615 } else {
616 $item_data['menu-item-object'] = (string) $item['object'];
617 $item_data['menu-item-object-id'] = (int) $item['object_id'];
618 }
619
620 $new_id = wp_update_nav_menu_item( $menu_id, 0, $item_data );
621 if ( is_wp_error( $new_id ) ) {
622 $warnings[] = sprintf( 'Failed to create item at index %d: %s', $idx, $new_id->get_error_message() );
623 continue;
624 }
625 if ( ! $new_id ) {
626 $warnings[] = sprintf( 'Failed to create item at index %d (no ID returned).', $idx );
627 continue;
628 }
629
630 // Restore third-party meta passed back from get-menu.
631 if ( isset( $item['meta'] ) && is_array( $item['meta'] ) ) {
632 foreach ( $item['meta'] as $mk => $mv ) {
633 if ( ! is_string( $mk ) || $mk === '' ) continue;
634 // Don't allow override of internal nav_menu meta keys.
635 if ( strpos( $mk, '_menu_item_' ) === 0 ) continue;
636 update_post_meta( (int) $new_id, $mk, $mv );
637 }
638 }
639
640 $index_to_new_id[ $idx ] = (int) $new_id;
641 $created_count++;
642 }
643
644 return [
645 'success' => true,
646 'menu_id' => $menu_id,
647 'created' => $created_count,
648 'deleted' => $deleted_count,
649 'warnings' => $warnings,
650 'message' => sprintf( 'Menu %d updated: %d item(s) created, %d removed.%s', $menu_id, $created_count, $deleted_count, empty( $warnings ) ? '' : sprintf( ' %d warning(s).', count( $warnings ) ) ),
651 ];
652 },
653 'permission_callback' => function() {
654 return current_user_can( 'edit_theme_options' );
655 },
656 'meta' => [
657 'mcp' => [ 'public' => true, 'type' => 'tool' ],
658 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => true ],
659 ],
660 ] );
661
662 // ---- list-menu-locations ----
663 wp_register_ability( 'atarim/list-menu-locations', [
664 'label' => 'List Menu Locations',
665 'description' => 'Returns theme-registered menu locations (e.g. "primary", "footer", "social") with the menu currently assigned to each (if any). A location with no assigned menu has assigned_menu_id = 0. The location keys come from the theme; the AI can use them with create-menu / update-menu assign_locations.',
666 'category' => 'atarim',
667 'input_schema' => [
668 'type' => 'object',
669 'properties' => [],
670 'additionalProperties' => false,
671 ],
672 'output_schema' => [
673 'type' => 'object',
674 'properties' => [
675 'success' => [ 'type' => 'boolean' ],
676 'total' => [ 'type' => 'integer' ],
677 'locations' => [ 'type' => 'array' ],
678 'message' => [ 'type' => 'string' ],
679 ],
680 'required' => [ 'success', 'message' ],
681 ],
682 'execute_callback' => function( $input = [] ) {
683 $registered = (array) get_registered_nav_menus();
684 $assignments = (array) get_nav_menu_locations();
685
686 $locations = [];
687 foreach ( $registered as $slug => $label ) {
688 $assigned_id = isset( $assignments[ $slug ] ) ? (int) $assignments[ $slug ] : 0;
689 $assigned_name = '';
690 if ( $assigned_id > 0 ) {
691 $m = wp_get_nav_menu_object( $assigned_id );
692 $assigned_name = $m ? (string) $m->name : '';
693 }
694 $locations[] = [
695 'slug' => (string) $slug,
696 'label' => (string) $label,
697 'assigned_menu_id' => $assigned_id,
698 'assigned_menu_name'=> $assigned_name,
699 ];
700 }
701
702 return [
703 'success' => true,
704 'total' => count( $locations ),
705 'locations' => $locations,
706 'message' => sprintf( '%d menu location(s) registered by the active theme.', count( $locations ) ),
707 ];
708 },
709 'permission_callback' => function() {
710 return current_user_can( 'edit_theme_options' );
711 },
712 'meta' => [
713 'mcp' => [ 'public' => true, 'type' => 'tool' ],
714 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
715 ],
716 ] );
717
718 // ---- list-widget-areas ----
719 wp_register_ability( 'atarim/list-widget-areas', [
720 'label' => 'List Widget Areas',
721 'description' => 'Returns all theme-registered widget areas (sidebars) with the widgets currently in each. Per-widget type detection: each widget reports its own type ("classic" — a traditional widget like text-1 or nav_menu-3, with settings; or "block" — a block-based widget, with block_markup). A single sidebar can contain both types (common when a pre-WP-5.8 site adopts block widgets gradually). Use update-widget-area to replace the widget set.',
722 'category' => 'atarim',
723 'input_schema' => [
724 'type' => 'object',
725 'properties' => [],
726 'additionalProperties' => false,
727 ],
728 'output_schema' => [
729 'type' => 'object',
730 'properties' => [
731 'success' => [ 'type' => 'boolean' ],
732 'total' => [ 'type' => 'integer' ],
733 'sidebars' => [ 'type' => 'array' ],
734 'block_widgets_used'=> [ 'type' => 'boolean' ],
735 'message' => [ 'type' => 'string' ],
736 ],
737 'required' => [ 'success', 'message' ],
738 ],
739 'execute_callback' => function( $input = [] ) {
740 global $wp_registered_sidebars;
741
742 $sidebars_widgets = wp_get_sidebars_widgets();
743 $registered = is_array( $wp_registered_sidebars ) ? $wp_registered_sidebars : [];
744
745 $sidebars = [];
746 $any_block = false;
747
748 foreach ( $registered as $sidebar_id => $sidebar_def ) {
749 if ( $sidebar_id === 'wp_inactive_widgets' ) continue;
750 $widget_ids = isset( $sidebars_widgets[ $sidebar_id ] ) ? (array) $sidebars_widgets[ $sidebar_id ] : [];
751 $widgets = [];
752 foreach ( $widget_ids as $widget_id ) {
753 $info = $this->avcf_unpack_widget( (string) $widget_id );
754 if ( $info['type'] === 'block' ) $any_block = true;
755 $widgets[] = $info;
756 }
757 $sidebars[] = [
758 'id' => (string) $sidebar_id,
759 'name' => isset( $sidebar_def['name'] ) ? (string) $sidebar_def['name'] : '',
760 'description' => isset( $sidebar_def['description'] ) ? (string) $sidebar_def['description'] : '',
761 'widget_count' => count( $widgets ),
762 'widgets' => $widgets,
763 ];
764 }
765
766 // Inactive widgets — surface as a special pseudo-sidebar so the AI can see what's parked.
767 if ( isset( $sidebars_widgets['wp_inactive_widgets'] ) && ! empty( $sidebars_widgets['wp_inactive_widgets'] ) ) {
768 $inactive = [];
769 foreach ( (array) $sidebars_widgets['wp_inactive_widgets'] as $widget_id ) {
770 $info = $this->avcf_unpack_widget( (string) $widget_id );
771 $inactive[] = $info;
772 }
773 $sidebars[] = [
774 'id' => 'wp_inactive_widgets',
775 'name' => 'Inactive Widgets',
776 'description' => 'Widgets not currently assigned to any active sidebar.',
777 'widget_count' => count( $inactive ),
778 'widgets' => $inactive,
779 ];
780 }
781
782 return [
783 'success' => true,
784 'total' => count( $sidebars ),
785 'sidebars' => $sidebars,
786 'block_widgets_used'=> $any_block,
787 'message' => sprintf( '%d sidebar(s); block widgets %s', count( $sidebars ), $any_block ? 'in use somewhere' : 'not currently used' ),
788 ];
789 },
790 'permission_callback' => function() {
791 return current_user_can( 'edit_theme_options' );
792 },
793 'meta' => [
794 'mcp' => [ 'public' => true, 'type' => 'tool' ],
795 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
796 ],
797 ] );
798
799 // ---- update-widget-area ----
800 wp_register_ability( 'atarim/update-widget-area', [
801 'label' => 'Update Widget Area',
802 'description' => 'REPLACE the widget set for one sidebar. items is an array of widget instances in order. Each item: { type: "classic", widget_type: "text", settings: {title: "...", text: "..."} } OR { type: "block", block_markup: "<!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph -->" }. For classic widgets, settings keys depend on the widget_type — see get-widget-types and existing classic widgets in list-widget-areas for shape. For block widgets, block_markup must be valid WordPress block markup (gets validated via parse_blocks). Existing widgets in the sidebar are removed; their underlying settings remain in widget_{type} options for forensics but are no longer referenced. Empty items array clears the sidebar.',
803 'category' => 'atarim',
804 'input_schema' => [
805 'type' => 'object',
806 'properties' => [
807 'sidebar_id' => [ 'type' => 'string', 'minLength' => 1 ],
808 'items' => [
809 'type' => 'array',
810 'description' => 'New widget set. Empty array clears the sidebar.',
811 'items' => [
812 'type' => 'object',
813 'properties' => [
814 'type' => [ 'type' => 'string', 'enum' => [ 'classic', 'block' ] ],
815 'widget_type' => [ 'type' => 'string', 'description' => 'For type=classic: the widget type ID (text, nav_menu, categories, etc).' ],
816 'settings' => [ 'type' => 'object', 'description' => 'For type=classic: the widget instance settings.' ],
817 'block_markup' => [ 'type' => 'string', 'description' => 'For type=block: valid WordPress block markup.' ],
818 ],
819 'required' => [ 'type' ],
820 ],
821 ],
822 ],
823 'required' => [ 'sidebar_id', 'items' ],
824 'additionalProperties' => false,
825 ],
826 'output_schema' => [
827 'type' => 'object',
828 'properties' => [
829 'success' => [ 'type' => 'boolean' ],
830 'sidebar_id' => [ 'type' => 'string' ],
831 'created' => [ 'type' => 'integer' ],
832 'removed' => [ 'type' => 'integer' ],
833 'warnings' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
834 'message' => [ 'type' => 'string' ],
835 ],
836 'required' => [ 'success', 'message' ],
837 ],
838 'execute_callback' => function( $input = [] ) {
839 global $wp_registered_sidebars, $wp_widget_factory;
840
841 $sidebar_id = isset( $input['sidebar_id'] ) ? (string) $input['sidebar_id'] : '';
842 if ( $sidebar_id === '' ) {
843 return [ 'success' => false, 'message' => 'sidebar_id is required.' ];
844 }
845 if ( ! is_array( $wp_registered_sidebars ) || ! isset( $wp_registered_sidebars[ $sidebar_id ] ) ) {
846 return [ 'success' => false, 'message' => sprintf( 'Sidebar "%s" is not registered by the active theme.', $sidebar_id ) ];
847 }
848 $items_in = isset( $input['items'] ) && is_array( $input['items'] ) ? $input['items'] : null;
849 if ( $items_in === null ) {
850 return [ 'success' => false, 'message' => 'items is required (empty array to clear).' ];
851 }
852
853 $warnings = [];
854
855 $sidebars_widgets = wp_get_sidebars_widgets();
856 $existing_in_sidebar = isset( $sidebars_widgets[ $sidebar_id ] ) ? (array) $sidebars_widgets[ $sidebar_id ] : [];
857 $removed_count = count( $existing_in_sidebar );
858
859 $new_widget_ids = [];
860
861 foreach ( $items_in as $idx => $item ) {
862 if ( ! is_array( $item ) || empty( $item['type'] ) ) {
863 $warnings[] = sprintf( 'Item at index %d is malformed — skipped.', $idx );
864 continue;
865 }
866
867 if ( $item['type'] === 'classic' ) {
868 if ( empty( $item['widget_type'] ) ) {
869 $warnings[] = sprintf( 'Item at index %d is type=classic but missing widget_type — skipped.', $idx );
870 continue;
871 }
872 $widget_type = sanitize_key( (string) $item['widget_type'] );
873 $settings = isset( $item['settings'] ) && is_array( $item['settings'] ) ? $item['settings'] : [];
874
875 // Validate widget_type exists in the factory.
876 $type_exists = false;
877 if ( $wp_widget_factory && ! empty( $wp_widget_factory->widgets ) ) {
878 foreach ( $wp_widget_factory->widgets as $w ) {
879 if ( method_exists( $w, 'id_base' ) ) {
880 if ( $w->id_base === $widget_type ) { $type_exists = true; break; }
881 } elseif ( property_exists( $w, 'id_base' ) ) {
882 if ( $w->id_base === $widget_type ) { $type_exists = true; break; }
883 }
884 }
885 }
886 if ( ! $type_exists ) {
887 $warnings[] = sprintf( 'Item at index %d uses widget_type "%s" which is not registered — skipped.', $idx, $widget_type );
888 continue;
889 }
890
891 // Allocate a new instance number for this widget_type.
892 $option_key = 'widget_' . $widget_type;
893 $instances = get_option( $option_key );
894 if ( ! is_array( $instances ) ) $instances = [];
895 $next_n = 1;
896 foreach ( array_keys( $instances ) as $k ) {
897 if ( is_numeric( $k ) && (int) $k >= $next_n ) {
898 $next_n = ( (int) $k ) + 1;
899 }
900 }
901 $instances[ $next_n ] = $settings;
902 // _multiwidget marker is required for multi-widget storage.
903 $instances['_multiwidget'] = 1;
904 update_option( $option_key, $instances );
905
906 $new_widget_ids[] = $widget_type . '-' . $next_n;
907 continue;
908 }
909
910 if ( $item['type'] === 'block' ) {
911 $markup = isset( $item['block_markup'] ) ? (string) $item['block_markup'] : '';
912 if ( $markup === '' ) {
913 $warnings[] = sprintf( 'Item at index %d is type=block but block_markup is empty — skipped.', $idx );
914 continue;
915 }
916 // Validate parseable.
917 $parsed = function_exists( 'parse_blocks' ) ? parse_blocks( $markup ) : null;
918 if ( ! is_array( $parsed ) || empty( $parsed ) ) {
919 $warnings[] = sprintf( 'Item at index %d block_markup is not valid block markup — skipped.', $idx );
920 continue;
921 }
922
923 // Block widgets are stored as widget_block instances with { content: markup }.
924 $option_key = 'widget_block';
925 $instances = get_option( $option_key );
926 if ( ! is_array( $instances ) ) $instances = [];
927 $next_n = 1;
928 foreach ( array_keys( $instances ) as $k ) {
929 if ( is_numeric( $k ) && (int) $k >= $next_n ) {
930 $next_n = ( (int) $k ) + 1;
931 }
932 }
933 $instances[ $next_n ] = [ 'content' => $markup ];
934 $instances['_multiwidget'] = 1;
935 update_option( $option_key, $instances );
936
937 $new_widget_ids[] = 'block-' . $next_n;
938 continue;
939 }
940
941 $warnings[] = sprintf( 'Item at index %d has unknown type "%s" — skipped.', $idx, $item['type'] );
942 }
943
944 // Update the sidebars_widgets option for this sidebar only.
945 $sidebars_widgets[ $sidebar_id ] = $new_widget_ids;
946 wp_set_sidebars_widgets( $sidebars_widgets );
947
948 return [
949 'success' => true,
950 'sidebar_id' => $sidebar_id,
951 'created' => count( $new_widget_ids ),
952 'removed' => $removed_count,
953 'warnings' => $warnings,
954 'message' => sprintf( 'Sidebar "%s" updated: %d widget(s) set, %d removed.%s', $sidebar_id, count( $new_widget_ids ), $removed_count, empty( $warnings ) ? '' : sprintf( ' %d warning(s).', count( $warnings ) ) ),
955 ];
956 },
957 'permission_callback' => function() {
958 return current_user_can( 'edit_theme_options' );
959 },
960 'meta' => [
961 'mcp' => [ 'public' => true, 'type' => 'tool' ],
962 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => false ],
963 ],
964 ] );
965
966 // ---- get-widget-types ----
967 wp_register_ability( 'atarim/get-widget-types', [
968 'label' => 'Get Widget Types',
969 'description' => 'Returns the catalogue of classic widget TYPES registered on the site (text, nav_menu, categories, recent_posts, etc., plus any added by plugins). Each entry shows the type ID and human-readable name. Block widgets are not enumerated here because they use the full Gutenberg block library — for block widgets, use any block type from the standard WordPress core/* block namespace.',
970 'category' => 'atarim',
971 'input_schema' => [
972 'type' => 'object',
973 'properties' => [],
974 'additionalProperties' => false,
975 ],
976 'output_schema' => [
977 'type' => 'object',
978 'properties' => [
979 'success' => [ 'type' => 'boolean' ],
980 'total' => [ 'type' => 'integer' ],
981 'types' => [ 'type' => 'array' ],
982 'message' => [ 'type' => 'string' ],
983 ],
984 'required' => [ 'success', 'message' ],
985 ],
986 'execute_callback' => function( $input = [] ) {
987 global $wp_widget_factory;
988 $types = [];
989 if ( $wp_widget_factory && ! empty( $wp_widget_factory->widgets ) ) {
990 foreach ( $wp_widget_factory->widgets as $widget ) {
991 $id_base = property_exists( $widget, 'id_base' ) ? (string) $widget->id_base : '';
992 $name = property_exists( $widget, 'name' ) ? (string) $widget->name : '';
993 $desc = ( property_exists( $widget, 'widget_options' ) && is_array( $widget->widget_options ) && isset( $widget->widget_options['description'] ) )
994 ? (string) $widget->widget_options['description']
995 : '';
996 if ( $id_base === '' ) continue;
997 $types[] = [
998 'id_base' => $id_base,
999 'name' => $name,
1000 'description' => $desc,
1001 ];
1002 }
1003 }
1004 usort( $types, function( $a, $b ) {
1005 return strcmp( $a['id_base'], $b['id_base'] );
1006 } );
1007
1008 return [
1009 'success' => true,
1010 'total' => count( $types ),
1011 'types' => $types,
1012 'message' => sprintf( '%d classic widget type(s) registered.', count( $types ) ),
1013 ];
1014 },
1015 'permission_callback' => function() {
1016 return current_user_can( 'edit_theme_options' );
1017 },
1018 'meta' => [
1019 'mcp' => [ 'public' => true, 'type' => 'tool' ],
1020 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
1021 ],
1022 ] );
1023 }
1024
1025 /**
1026 * Unpack a widget instance ID like "text-3" or "block-1" into its
1027 * type, settings, and (for block widgets) block markup.
1028 *
1029 * @param string $widget_id
1030 * @return array
1031 */
1032 private function avcf_unpack_widget( $widget_id ) {
1033 // Widget IDs are {id_base}-{n}, where id_base may itself contain hyphens (rare but valid).
1034 if ( ! preg_match( '/^(.+)-(\d+)$/', $widget_id, $m ) ) {
1035 return [
1036 'id' => $widget_id,
1037 'type' => 'classic',
1038 'widget_type' => $widget_id,
1039 'instance_n' => 0,
1040 'settings' => [],
1041 ];
1042 }
1043 $id_base = $m[1];
1044 $n = (int) $m[2];
1045
1046 if ( $id_base === 'block' ) {
1047 $instances = get_option( 'widget_block' );
1048 $markup = '';
1049 if ( is_array( $instances ) && isset( $instances[ $n ]['content'] ) ) {
1050 $markup = (string) $instances[ $n ]['content'];
1051 }
1052 return [
1053 'id' => $widget_id,
1054 'type' => 'block',
1055 'instance_n' => $n,
1056 'block_markup' => $markup,
1057 ];
1058 }
1059
1060 $instances = get_option( 'widget_' . $id_base );
1061 $settings = ( is_array( $instances ) && isset( $instances[ $n ] ) && is_array( $instances[ $n ] ) ) ? $instances[ $n ] : [];
1062
1063 return [
1064 'id' => $widget_id,
1065 'type' => 'classic',
1066 'widget_type' => $id_base,
1067 'instance_n' => $n,
1068 'settings' => $settings,
1069 ];
1070 }
1071 }
1072