PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.1.2
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.1.2
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 / third-party / page-builder / etch / class-avcf-abilities-etch-pro.php
atarim-visual-collaboration / third-party / page-builder / etch Last commit date
class-avcf-abilities-etch-pro.php 2 weeks ago class-avcf-abilities-etch.php 2 weeks ago class-avcf-etch-detector.php 2 weeks ago class-avcf-etch-helpers.php 2 weeks ago
class-avcf-abilities-etch-pro.php
867 lines
1 <?php
2 /**
3 * Etch — Advanced MCP abilities (the "pro" file). Pass 1 here is content modeling (post types, taxonomies, custom fields, loops, queries); the design pass (components, templates, styles) is appended to this same file.
4 *
5 * Post types (etch_cpts), taxonomies (etch_taxonomies), custom field groups +
6 * fields (etch_cfs), loops (etch_loops), and queries (etch_queries) — all plain
7 * WP option maps. Definition CRUD is reliable option read/modify/write; actual
8 * registration happens at Etch's runtime from these options. Site-wide writers
9 * are confirm-gated. Built from the Novamira reference; not runtime-tested.
10 *
11 * @package atarim-visual-collaboration
12 */
13
14 if ( ! defined('ABSPATH') ) {
15 exit;
16 }
17
18 class AVCF_Abilities_Etch_Pro extends AVCF_Abilities_Base {
19
20 const O_CPT = 'etch_cpts';
21 const O_TAX = 'etch_taxonomies';
22 const O_CF = 'etch_cfs';
23 const O_LOOP = 'etch_loops';
24 const O_QRY = 'etch_queries';
25 const O_STYLES = 'etch_styles';
26 const O_SHEETS = 'etch_global_stylesheets';
27 const COMPONENT_CPT = 'wp_block';
28 const TEMPLATE_TAX = 'wp_theme';
29
30 /** @var AVCF_Etch_Detector */
31 private $detector;
32
33 public function __construct() {
34 $this->detector = new AVCF_Etch_Detector();
35 }
36
37 public function register() {
38 if ( ! $this->detector->avcf_etch_is_available() ) {
39 return;
40 }
41 $this->register_post_types();
42 $this->register_taxonomies();
43 $this->register_custom_fields();
44 $this->register_loops();
45 $this->register_queries();
46 $this->register_components();
47 $this->register_templates();
48 $this->register_styles();
49 }
50
51 /** Resolve the Etch template CPT (the WP block-template type). */
52 public function template_cpt() {
53 return defined( 'ETCH_TEMPLATE_POST_TYPE' ) ? constant( 'ETCH_TEMPLATE_POST_TYPE' ) : 'wp_template';
54 }
55
56 /* ------------------------------ shared ----------------------------- */
57
58 private function ro_meta() {
59 return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ] ];
60 }
61 private function write_meta( $destructive = false ) {
62 return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => false, 'destructive' => (bool) $destructive, 'idempotent' => false ] ];
63 }
64 private function std_out() {
65 return [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ];
66 }
67 private function site_can() { return current_user_can( 'manage_options' ); }
68 private function confirm_gate( $input, $what ) {
69 if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'applied' => false, 'message' => sprintf( 'Dry run: %s is site-wide. Re-call with confirm:true.', $what ) ]; }
70 return null;
71 }
72 public function opt_get( $name ) { $v = get_option( $name, [] ); return is_array( $v ) ? $v : []; }
73 public function opt_set( $name, $map ) { return (bool) update_option( $name, $map ); }
74 public function gen_id() { return substr( md5( uniqid( '', true ) ), 0, 10 ); }
75
76 /* ------------------------ post types / taxonomies ------------------ */
77
78 /** Shared slug=>args map CRUD registrar for post types & taxonomies. */
79 private function register_slug_map( $opt, $singular, $extra_create = [] ) {
80 $self = $this; $std = $this->std_out();
81 $kind = $singular; // 'post-type' | 'taxonomy'
82 $noun = str_replace( '-', ' ', $singular );
83
84 wp_register_ability( 'atarim/etch-list-' . $kind . 's', [
85 'label' => 'List Etch ' . ucwords( $noun ) . 's', 'category' => 'atarim',
86 'description' => sprintf( 'List the %ss Etch registers (the %s option). Returns { slug, label } per entry; fetch full args with get-%s.', $noun, $opt, $kind ),
87 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
88 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'items' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
89 'execute_callback' => function( $input = [] ) use ( $self, $opt ) {
90 $map = $self->opt_get( $opt );
91 $out = [];
92 foreach ( $map as $slug => $args ) {
93 $label = is_array( $args ) ? ( $args['label'] ?? ( isset( $args['labels']['name'] ) ? $args['labels']['name'] : (string) $slug ) ) : (string) $slug;
94 $out[] = [ 'slug' => (string) $slug, 'label' => $label ];
95 }
96 return [ 'success' => true, 'items' => $out, 'message' => sprintf( '%d %s(s).', count( $out ), $noun ) ];
97 },
98 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
99 'meta' => $this->ro_meta(),
100 ] );
101
102 wp_register_ability( 'atarim/etch-get-' . $kind, [
103 'label' => 'Get Etch ' . ucwords( $noun ), 'category' => 'atarim',
104 'description' => sprintf( 'Get the full registration args for one %s by slug.', $noun ),
105 'input_schema' => [ 'type' => 'object', 'properties' => [ 'slug' => [ 'type' => 'string' ] ], 'required' => [ 'slug' ], 'additionalProperties' => false ],
106 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'slug' => [ 'type' => 'string' ], 'args' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
107 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun ) {
108 $map = $self->opt_get( $opt ); $slug = (string) $input['slug'];
109 if ( ! isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( '%s "%s" not found.', ucfirst( $noun ), $slug ) ]; }
110 return [ 'success' => true, 'slug' => $slug, 'args' => (array) $map[ $slug ], 'message' => 'OK.' ];
111 },
112 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
113 'meta' => $this->ro_meta(),
114 ] );
115
116 $create_props = array_merge( [
117 'slug' => [ 'type' => 'string' ],
118 'label' => [ 'type' => 'string' ],
119 'args' => [ 'type' => 'object', 'description' => 'Full register_' . str_replace( '-', '_', $kind ) . ' args (merged over sensible defaults).' ],
120 'confirm' => [ 'type' => 'boolean', 'default' => false ],
121 ], $extra_create );
122
123 wp_register_ability( 'atarim/etch-create-' . $kind, [
124 'label' => 'Create Etch ' . ucwords( $noun ), 'category' => 'atarim',
125 'description' => sprintf( 'SITE-WIDE. Register a new %s. slug + label required. args is the full registration args object (merged over defaults). Dry run unless confirm:true.', $noun ) . ( $kind === 'taxonomy' ? ' object_type is the array of post-type slugs this taxonomy attaches to.' : '' ),
126 'input_schema' => [ 'type' => 'object', 'properties' => $create_props, 'required' => [ 'slug', 'label' ], 'additionalProperties' => false ],
127 'output_schema'=> $std,
128 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun, $kind ) {
129 $gate = $self->confirm_gate( $input, 'Registering a ' . $noun ); if ( $gate ) { return $gate; }
130 $slug = sanitize_key( (string) $input['slug'] );
131 if ( $slug === '' ) { return [ 'success' => false, 'message' => 'A valid slug is required.' ]; }
132 $map = $self->opt_get( $opt );
133 if ( isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( 'A %s with slug "%s" already exists.', $noun, $slug ) ]; }
134 $args = isset( $input['args'] ) && is_array( $input['args'] ) ? $input['args'] : [];
135 $args['label'] = (string) $input['label'];
136 if ( $kind === 'taxonomy' && isset( $input['object_type'] ) && is_array( $input['object_type'] ) ) { $args['object_type'] = array_values( array_map( 'sanitize_key', $input['object_type'] ) ); }
137 if ( ! isset( $args['public'] ) ) { $args['public'] = true; }
138 if ( ! isset( $args['show_in_rest'] ) ) { $args['show_in_rest'] = true; }
139 if ( $kind === 'post-type' && ! isset( $args['supports'] ) ) { $args['supports'] = [ 'title', 'editor' ]; }
140 $map[ $slug ] = $args;
141 if ( ! $self->opt_set( $opt, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
142 return [ 'success' => true, 'message' => sprintf( '%s "%s" created (takes effect after Etch re-registers / next load).', ucfirst( $noun ), $slug ) ];
143 },
144 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
145 'meta' => $this->write_meta( false ),
146 ] );
147
148 wp_register_ability( 'atarim/etch-edit-' . $kind, [
149 'label' => 'Edit Etch ' . ucwords( $noun ), 'category' => 'atarim',
150 'description' => sprintf( 'SITE-WIDE. Edit a %s by slug (args merged; label convenience). Dry run unless confirm:true.', $noun ),
151 'input_schema' => [ 'type' => 'object', 'properties' => [ 'slug' => [ 'type' => 'string' ], 'label' => [ 'type' => 'string' ], 'args' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'slug' ], 'additionalProperties' => false ],
152 'output_schema'=> $std,
153 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun ) {
154 $gate = $self->confirm_gate( $input, 'Editing a ' . $noun ); if ( $gate ) { return $gate; }
155 $map = $self->opt_get( $opt ); $slug = (string) $input['slug'];
156 if ( ! isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( '%s "%s" not found.', ucfirst( $noun ), $slug ) ]; }
157 $args = is_array( $map[ $slug ] ) ? $map[ $slug ] : [];
158 if ( isset( $input['args'] ) && is_array( $input['args'] ) ) { $args = array_merge( $args, $input['args'] ); }
159 if ( isset( $input['label'] ) ) { $args['label'] = (string) $input['label']; }
160 $map[ $slug ] = $args;
161 if ( ! $self->opt_set( $opt, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
162 return [ 'success' => true, 'message' => sprintf( '%s "%s" updated.', ucfirst( $noun ), $slug ) ];
163 },
164 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
165 'meta' => $this->write_meta( false ),
166 ] );
167
168 wp_register_ability( 'atarim/etch-delete-' . $kind, [
169 'label' => 'Delete Etch ' . ucwords( $noun ), 'category' => 'atarim',
170 'description' => sprintf( 'SITE-WIDE. Unregister a %s by slug (removes the definition; existing content is not deleted). Dry run unless confirm:true.', $noun ),
171 'input_schema' => [ 'type' => 'object', 'properties' => [ 'slug' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'slug' ], 'additionalProperties' => false ],
172 'output_schema'=> $std,
173 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun ) {
174 $gate = $self->confirm_gate( $input, 'Deleting a ' . $noun ); if ( $gate ) { return $gate; }
175 $map = $self->opt_get( $opt ); $slug = (string) $input['slug'];
176 if ( ! isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( '%s "%s" not found.', ucfirst( $noun ), $slug ) ]; }
177 unset( $map[ $slug ] );
178 if ( ! $self->opt_set( $opt, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
179 return [ 'success' => true, 'message' => sprintf( '%s "%s" deleted.', ucfirst( $noun ), $slug ) ];
180 },
181 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
182 'meta' => $this->write_meta( true ),
183 ] );
184 }
185
186 private function register_post_types() {
187 $this->register_slug_map( self::O_CPT, 'post-type' );
188 }
189 private function register_taxonomies() {
190 $this->register_slug_map( self::O_TAX, 'taxonomy', [ 'object_type' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Post-type slugs this taxonomy attaches to.' ] ] );
191 }
192
193 /* --------------------------- custom fields ------------------------- */
194
195 private function register_custom_fields() {
196 $self = $this; $std = $this->std_out();
197
198 wp_register_ability( 'atarim/etch-list-field-groups', [
199 'label' => 'List Etch Field Groups', 'category' => 'atarim',
200 'description' => 'List Etch custom field groups (etch_cfs): { id, label, post_types, field_count }. Fetch full definitions with get-field-group.',
201 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
202 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'groups' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
203 'execute_callback' => function( $input = [] ) use ( $self ) {
204 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF );
205 $out = [];
206 foreach ( $map as $id => $g ) {
207 if ( ! is_array( $g ) ) { continue; }
208 $fields = isset( $g['fields'] ) && is_array( $g['fields'] ) ? $g['fields'] : [];
209 $out[] = [ 'id' => (string) $id, 'label' => $g['label'] ?? '', 'post_types' => $g['post_types'] ?? ( $g['assigned_to'] ?? [] ), 'field_count' => count( $fields ) ];
210 }
211 return [ 'success' => true, 'groups' => $out, 'message' => sprintf( '%d group(s).', count( $out ) ) ];
212 },
213 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
214 'meta' => $this->ro_meta(),
215 ] );
216
217 wp_register_ability( 'atarim/etch-get-field-group', [
218 'label' => 'Get Etch Field Group', 'category' => 'atarim',
219 'description' => 'Get a field group (label, post_types, and its fields) by id.',
220 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
221 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'group' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
222 'execute_callback' => function( $input = [] ) use ( $self ) {
223 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $id = (string) $input['id'];
224 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $id ) ]; }
225 return [ 'success' => true, 'group' => array_merge( [ 'id' => $id ], (array) $map[ $id ] ), 'message' => 'OK.' ];
226 },
227 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
228 'meta' => $this->ro_meta(),
229 ] );
230
231 wp_register_ability( 'atarim/etch-create-field-group', [
232 'label' => 'Create Etch Field Group', 'category' => 'atarim',
233 'description' => 'SITE-WIDE. Create a custom field group. label required; post_types is the array of post-type slugs to attach to; fields is an optional initial array of field defs ({key, label, type, ...}). Dry run unless confirm:true.',
234 'input_schema' => [ 'type' => 'object', 'properties' => [ 'label' => [ 'type' => 'string' ], 'post_types' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], 'fields' => [ 'type' => 'array' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'label' ], 'additionalProperties' => false ],
235 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
236 'execute_callback' => function( $input = [] ) use ( $self ) {
237 $gate = $self->confirm_gate( $input, 'Creating a field group' ); if ( $gate ) { return $gate; }
238 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF );
239 $id = $self->gen_id();
240 $map[ $id ] = [
241 'label' => (string) $input['label'],
242 'post_types' => isset( $input['post_types'] ) && is_array( $input['post_types'] ) ? array_values( array_map( 'sanitize_key', $input['post_types'] ) ) : [],
243 'fields' => isset( $input['fields'] ) && is_array( $input['fields'] ) ? array_values( $input['fields'] ) : [],
244 ];
245 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
246 return [ 'success' => true, 'id' => $id, 'message' => 'Field group created.' ];
247 },
248 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
249 'meta' => $this->write_meta( false ),
250 ] );
251
252 wp_register_ability( 'atarim/etch-edit-field-group', [
253 'label' => 'Edit Etch Field Group', 'category' => 'atarim',
254 'description' => 'SITE-WIDE. Edit a field group by id (label and/or post_types). To change fields use add/edit/delete-field. Dry run unless confirm:true.',
255 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'label' => [ 'type' => 'string' ], 'post_types' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
256 'output_schema'=> $std,
257 'execute_callback' => function( $input = [] ) use ( $self ) {
258 $gate = $self->confirm_gate( $input, 'Editing a field group' ); if ( $gate ) { return $gate; }
259 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $id = (string) $input['id'];
260 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $id ) ]; }
261 if ( isset( $input['label'] ) ) { $map[ $id ]['label'] = (string) $input['label']; }
262 if ( isset( $input['post_types'] ) && is_array( $input['post_types'] ) ) { $map[ $id ]['post_types'] = array_values( array_map( 'sanitize_key', $input['post_types'] ) ); }
263 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
264 return [ 'success' => true, 'message' => 'Field group updated.' ];
265 },
266 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
267 'meta' => $this->write_meta( false ),
268 ] );
269
270 wp_register_ability( 'atarim/etch-delete-field-group', [
271 'label' => 'Delete Etch Field Group', 'category' => 'atarim',
272 'description' => 'SITE-WIDE. Delete a field group (and its fields) by id. Dry run unless confirm:true.',
273 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
274 'output_schema'=> $std,
275 'execute_callback' => function( $input = [] ) use ( $self ) {
276 $gate = $self->confirm_gate( $input, 'Deleting a field group' ); if ( $gate ) { return $gate; }
277 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $id = (string) $input['id'];
278 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $id ) ]; }
279 unset( $map[ $id ] );
280 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
281 return [ 'success' => true, 'message' => 'Field group deleted.' ];
282 },
283 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
284 'meta' => $this->write_meta( true ),
285 ] );
286
287 wp_register_ability( 'atarim/etch-add-field', [
288 'label' => 'Add Etch Field', 'category' => 'atarim',
289 'description' => 'SITE-WIDE. Add a field to a field group. field is the field def ({key, label, type, ...}); key must be unique within the group. Dry run unless confirm:true.',
290 'input_schema' => [ 'type' => 'object', 'properties' => [ 'group_id' => [ 'type' => 'string' ], 'field' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'group_id', 'field' ], 'additionalProperties' => false ],
291 'output_schema'=> $std,
292 'execute_callback' => function( $input = [] ) use ( $self ) {
293 $gate = $self->confirm_gate( $input, 'Adding a field' ); if ( $gate ) { return $gate; }
294 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $gid = (string) $input['group_id'];
295 if ( ! isset( $map[ $gid ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $gid ) ]; }
296 $field = is_array( $input['field'] ) ? $input['field'] : [];
297 $key = isset( $field['key'] ) ? (string) $field['key'] : '';
298 if ( $key === '' ) { return [ 'success' => false, 'message' => 'field.key is required.' ]; }
299 $fields = isset( $map[ $gid ]['fields'] ) && is_array( $map[ $gid ]['fields'] ) ? $map[ $gid ]['fields'] : [];
300 foreach ( $fields as $f ) { if ( is_array( $f ) && ( ( $f['key'] ?? null ) === $key ) ) { return [ 'success' => false, 'message' => sprintf( 'A field with key "%s" already exists in this group.', $key ) ]; } }
301 $fields[] = $field;
302 $map[ $gid ]['fields'] = array_values( $fields );
303 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
304 return [ 'success' => true, 'message' => sprintf( 'Field "%s" added.', $key ) ];
305 },
306 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
307 'meta' => $this->write_meta( false ),
308 ] );
309
310 wp_register_ability( 'atarim/etch-edit-field', [
311 'label' => 'Edit Etch Field', 'category' => 'atarim',
312 'description' => 'SITE-WIDE. Edit a field within a group by key. patch is merged into the field def. Dry run unless confirm:true.',
313 'input_schema' => [ 'type' => 'object', 'properties' => [ 'group_id' => [ 'type' => 'string' ], 'key' => [ 'type' => 'string' ], 'patch' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'group_id', 'key', 'patch' ], 'additionalProperties' => false ],
314 'output_schema'=> $std,
315 'execute_callback' => function( $input = [] ) use ( $self ) {
316 $gate = $self->confirm_gate( $input, 'Editing a field' ); if ( $gate ) { return $gate; }
317 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $gid = (string) $input['group_id']; $key = (string) $input['key'];
318 if ( ! isset( $map[ $gid ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $gid ) ]; }
319 $fields = isset( $map[ $gid ]['fields'] ) && is_array( $map[ $gid ]['fields'] ) ? $map[ $gid ]['fields'] : [];
320 $found = false;
321 foreach ( $fields as &$f ) { if ( is_array( $f ) && ( ( $f['key'] ?? null ) === $key ) ) { $f = array_merge( $f, is_array( $input['patch'] ) ? $input['patch'] : [] ); $f['key'] = $key; $found = true; break; } }
322 unset( $f );
323 if ( ! $found ) { return [ 'success' => false, 'message' => sprintf( 'Field "%s" not found in group.', $key ) ]; }
324 $map[ $gid ]['fields'] = array_values( $fields );
325 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
326 return [ 'success' => true, 'message' => sprintf( 'Field "%s" updated.', $key ) ];
327 },
328 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
329 'meta' => $this->write_meta( false ),
330 ] );
331
332 wp_register_ability( 'atarim/etch-delete-field', [
333 'label' => 'Delete Etch Field', 'category' => 'atarim',
334 'description' => 'SITE-WIDE. Remove a field from a group by key. Dry run unless confirm:true.',
335 'input_schema' => [ 'type' => 'object', 'properties' => [ 'group_id' => [ 'type' => 'string' ], 'key' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'group_id', 'key' ], 'additionalProperties' => false ],
336 'output_schema'=> $std,
337 'execute_callback' => function( $input = [] ) use ( $self ) {
338 $gate = $self->confirm_gate( $input, 'Deleting a field' ); if ( $gate ) { return $gate; }
339 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $gid = (string) $input['group_id']; $key = (string) $input['key'];
340 if ( ! isset( $map[ $gid ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $gid ) ]; }
341 $fields = isset( $map[ $gid ]['fields'] ) && is_array( $map[ $gid ]['fields'] ) ? $map[ $gid ]['fields'] : [];
342 $new = array_values( array_filter( $fields, function( $f ) use ( $key ) { return ! ( is_array( $f ) && ( ( $f['key'] ?? null ) === $key ) ); } ) );
343 if ( count( $new ) === count( $fields ) ) { return [ 'success' => false, 'message' => sprintf( 'Field "%s" not found in group.', $key ) ]; }
344 $map[ $gid ]['fields'] = $new;
345 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
346 return [ 'success' => true, 'message' => sprintf( 'Field "%s" deleted.', $key ) ];
347 },
348 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
349 'meta' => $this->write_meta( true ),
350 ] );
351
352 wp_register_ability( 'atarim/etch-list-dynamic-fields', [
353 'label' => 'List Etch Dynamic Fields', 'category' => 'atarim',
354 'description' => 'List the dynamic field keys available to bind in the builder — the custom fields defined across all Etch field groups ({ key, label, type, group }). Standard post fields (title, content, excerpt, featured image, etc.) are also available in Etch independently of this list.',
355 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
356 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'fields' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
357 'execute_callback' => function( $input = [] ) use ( $self ) {
358 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF );
359 $out = [];
360 foreach ( $map as $gid => $g ) {
361 if ( ! is_array( $g ) ) { continue; }
362 foreach ( ( isset( $g['fields'] ) && is_array( $g['fields'] ) ? $g['fields'] : [] ) as $f ) {
363 if ( ! is_array( $f ) ) { continue; }
364 $out[] = [ 'key' => $f['key'] ?? '', 'label' => $f['label'] ?? '', 'type' => $f['type'] ?? '', 'group' => (string) $gid ];
365 }
366 }
367 return [ 'success' => true, 'fields' => $out, 'message' => sprintf( '%d dynamic field(s).', count( $out ) ) ];
368 },
369 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
370 'meta' => $this->ro_meta(),
371 ] );
372 }
373
374 /* ------------------------------- loops ----------------------------- */
375
376 private function register_loops() {
377 $self = $this; $std = $this->std_out();
378
379 wp_register_ability( 'atarim/etch-list-loops', [
380 'label' => 'List Etch Loops', 'category' => 'atarim',
381 'description' => 'List Etch loops (etch_loops): { id, name, key }.',
382 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
383 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'loops' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
384 'execute_callback' => function( $input = [] ) use ( $self ) {
385 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP );
386 $out = [];
387 foreach ( $map as $id => $l ) { if ( is_array( $l ) ) { $out[] = [ 'id' => (string) $id, 'name' => $l['name'] ?? '', 'key' => $l['key'] ?? '' ]; } }
388 return [ 'success' => true, 'loops' => $out, 'message' => sprintf( '%d loop(s).', count( $out ) ) ];
389 },
390 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
391 'meta' => $this->ro_meta(),
392 ] );
393
394 wp_register_ability( 'atarim/etch-get-loop', [
395 'label' => 'Get Etch Loop', 'category' => 'atarim',
396 'description' => 'Get a loop (name, key, config) by id. config.type is e.g. "wp-query" with config.args being WP_Query args.',
397 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
398 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'loop' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
399 'execute_callback' => function( $input = [] ) use ( $self ) {
400 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); $id = (string) $input['id'];
401 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Loop "%s" not found.', $id ) ]; }
402 return [ 'success' => true, 'loop' => array_merge( [ 'id' => $id ], (array) $map[ $id ] ), 'message' => 'OK.' ];
403 },
404 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
405 'meta' => $this->ro_meta(),
406 ] );
407
408 wp_register_ability( 'atarim/etch-create-loop', [
409 'label' => 'Create Etch Loop', 'category' => 'atarim',
410 'description' => 'SITE-WIDE. Create a loop. name required; config is the loop config ({ type:"wp-query", args:{...WP_Query args} }) — model it on an existing loop (get-loop). Dry run unless confirm:true.',
411 'input_schema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string' ], 'config' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'name' ], 'additionalProperties' => false ],
412 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
413 'execute_callback' => function( $input = [] ) use ( $self ) {
414 $gate = $self->confirm_gate( $input, 'Creating a loop' ); if ( $gate ) { return $gate; }
415 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP );
416 $id = $self->gen_id();
417 $map[ $id ] = [ 'name' => (string) $input['name'], 'key' => $id, 'config' => isset( $input['config'] ) && is_array( $input['config'] ) ? $input['config'] : [ 'type' => 'wp-query', 'args' => [] ], 'global' => false ];
418 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_LOOP, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
419 return [ 'success' => true, 'id' => $id, 'message' => 'Loop created.' ];
420 },
421 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
422 'meta' => $this->write_meta( false ),
423 ] );
424
425 wp_register_ability( 'atarim/etch-edit-loop', [
426 'label' => 'Edit Etch Loop', 'category' => 'atarim',
427 'description' => 'SITE-WIDE. Edit a loop by id (name and/or config; config merged). Dry run unless confirm:true.',
428 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'name' => [ 'type' => 'string' ], 'config' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
429 'output_schema'=> $std,
430 'execute_callback' => function( $input = [] ) use ( $self ) {
431 $gate = $self->confirm_gate( $input, 'Editing a loop' ); if ( $gate ) { return $gate; }
432 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); $id = (string) $input['id'];
433 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Loop "%s" not found.', $id ) ]; }
434 if ( isset( $input['name'] ) ) { $map[ $id ]['name'] = (string) $input['name']; }
435 if ( isset( $input['config'] ) && is_array( $input['config'] ) ) {
436 $cur = isset( $map[ $id ]['config'] ) && is_array( $map[ $id ]['config'] ) ? $map[ $id ]['config'] : [];
437 $map[ $id ]['config'] = array_merge( $cur, $input['config'] );
438 }
439 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_LOOP, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
440 return [ 'success' => true, 'message' => 'Loop updated.' ];
441 },
442 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
443 'meta' => $this->write_meta( false ),
444 ] );
445
446 wp_register_ability( 'atarim/etch-delete-loop', [
447 'label' => 'Delete Etch Loop', 'category' => 'atarim',
448 'description' => 'SITE-WIDE. Delete a loop by id. Dry run unless confirm:true.',
449 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
450 'output_schema'=> $std,
451 'execute_callback' => function( $input = [] ) use ( $self ) {
452 $gate = $self->confirm_gate( $input, 'Deleting a loop' ); if ( $gate ) { return $gate; }
453 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); $id = (string) $input['id'];
454 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Loop "%s" not found.', $id ) ]; }
455 unset( $map[ $id ] );
456 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_LOOP, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
457 return [ 'success' => true, 'message' => 'Loop deleted.' ];
458 },
459 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
460 'meta' => $this->write_meta( true ),
461 ] );
462 }
463
464 /* ------------------------------ queries ---------------------------- */
465
466 private function register_queries() {
467 $self = $this;
468
469 wp_register_ability( 'atarim/etch-list-queries', [
470 'label' => 'List Etch Queries', 'category' => 'atarim',
471 'description' => 'List saved Etch queries (etch_queries): { id, name }.',
472 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
473 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'queries' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
474 'execute_callback' => function( $input = [] ) use ( $self ) {
475 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_QRY );
476 $out = [];
477 foreach ( $map as $id => $q ) { if ( is_array( $q ) ) { $out[] = [ 'id' => (string) $id, 'name' => $q['name'] ?? '' ]; } }
478 return [ 'success' => true, 'queries' => $out, 'message' => sprintf( '%d query(ies).', count( $out ) ) ];
479 },
480 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
481 'meta' => $this->ro_meta(),
482 ] );
483
484 wp_register_ability( 'atarim/etch-get-query', [
485 'label' => 'Get Etch Query', 'category' => 'atarim',
486 'description' => 'Get a saved query (name, args) by id.',
487 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
488 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'query' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
489 'execute_callback' => function( $input = [] ) use ( $self ) {
490 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_QRY ); $id = (string) $input['id'];
491 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Query "%s" not found.', $id ) ]; }
492 return [ 'success' => true, 'query' => array_merge( [ 'id' => $id ], (array) $map[ $id ] ), 'message' => 'OK.' ];
493 },
494 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
495 'meta' => $this->ro_meta(),
496 ] );
497
498 wp_register_ability( 'atarim/etch-get-query-preview', [
499 'label' => 'Get Etch Query Preview', 'category' => 'atarim',
500 'description' => 'Run a query and return a preview of matched posts ({ id, title, type }). Provide query_id (a saved query) OR args (WP_Query args directly). Capped at 20 results.',
501 'input_schema' => [ 'type' => 'object', 'properties' => [ 'query_id' => [ 'type' => 'string' ], 'args' => [ 'type' => 'object' ] ], 'additionalProperties' => false ],
502 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'found_posts' => [ 'type' => 'integer' ], 'posts' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
503 'execute_callback' => function( $input = [] ) use ( $self ) {
504 $args = [];
505 if ( isset( $input['args'] ) && is_array( $input['args'] ) ) { $args = $input['args']; }
506 elseif ( isset( $input['query_id'] ) ) {
507 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_QRY ); $id = (string) $input['query_id'];
508 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Query "%s" not found.', $id ) ]; }
509 $args = isset( $map[ $id ]['args'] ) && is_array( $map[ $id ]['args'] ) ? $map[ $id ]['args'] : [];
510 } else {
511 return [ 'success' => false, 'message' => 'Provide query_id or args.' ];
512 }
513 $args['posts_per_page'] = min( 20, isset( $args['posts_per_page'] ) ? (int) $args['posts_per_page'] : 20 );
514 $args['fields'] = 'ids';
515 $q = new WP_Query( $args );
516 $posts = [];
517 foreach ( $q->posts as $pid ) { $posts[] = [ 'id' => (int) $pid, 'title' => get_the_title( $pid ), 'type' => get_post_type( $pid ) ]; }
518 return [ 'success' => true, 'found_posts' => (int) $q->found_posts, 'posts' => $posts, 'message' => sprintf( '%d found; showing %d.', (int) $q->found_posts, count( $posts ) ) ];
519 },
520 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
521 'meta' => $this->ro_meta(),
522 ] );
523 }
524
525 /* ---------------------------- components --------------------------- */
526
527 private function register_components() {
528 $self = $this; $std = $this->std_out();
529
530 wp_register_ability( 'atarim/etch-list-components', [
531 'label' => 'List Etch Components', 'category' => 'atarim',
532 'description' => 'List Etch components (reusable-block-backed): { id, name, key }. Components are wp_block posts carrying Etch component metadata.',
533 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
534 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'components' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
535 'execute_callback' => function( $input = [] ) {
536 $posts = get_posts( [ 'post_type' => AVCF_Abilities_Etch_Pro::COMPONENT_CPT, 'post_status' => 'any', 'numberposts' => -1, 'meta_key' => 'etch_component_html_key' ] );
537 $out = [];
538 foreach ( $posts as $p ) { $out[] = [ 'id' => $p->ID, 'name' => $p->post_title, 'key' => (string) get_post_meta( $p->ID, 'etch_component_html_key', true ) ]; }
539 return [ 'success' => true, 'components' => $out, 'message' => sprintf( '%d component(s).', count( $out ) ) ];
540 },
541 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
542 'meta' => $this->ro_meta(),
543 ] );
544
545 wp_register_ability( 'atarim/etch-get-component', [
546 'label' => 'Get Etch Component', 'category' => 'atarim',
547 'description' => 'Get one component by id: name, key, description, properties, and its element tree (parsed from the block content). lossy:true means it holds freeform HTML the tree can\'t represent.',
548 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
549 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'component' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
550 'execute_callback' => function( $input = [] ) {
551 $id = (int) $input['id']; $p = get_post( $id );
552 if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; }
553 $props = get_post_meta( $id, 'etch_component_properties', true );
554 $tree = AVCF_Etch_Helpers::parse_tree( $p->post_content );
555 return [ 'success' => true, 'component' => [
556 'id' => $id, 'name' => $p->post_title, 'key' => (string) get_post_meta( $id, 'etch_component_html_key', true ),
557 'description' => $p->post_excerpt, 'properties' => is_array( $props ) ? $props : [],
558 'tree' => $tree, 'node_count' => AVCF_Etch_Helpers::tree_count( $tree ), 'lossy' => AVCF_Etch_Helpers::is_lossy( $p->post_content ),
559 ], 'message' => 'OK.' ];
560 },
561 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
562 'meta' => $this->ro_meta(),
563 ] );
564
565 wp_register_ability( 'atarim/etch-create-component', [
566 'label' => 'Create Etch Component', 'category' => 'atarim',
567 'description' => 'SITE-WIDE. Create a component. name required; description optional; tree is the element tree ([{block,attrs?,children?,html?}], serialized to block content); properties is the component\'s props object. Dry run unless confirm:true.',
568 'input_schema' => [ 'type' => 'object', 'properties' => [
569 'name' => [ 'type' => 'string' ], 'description' => [ 'type' => 'string' ],
570 'tree' => [ 'type' => 'array' ], 'properties' => [ 'type' => 'object' ],
571 'confirm' => [ 'type' => 'boolean', 'default' => false ],
572 ], 'required' => [ 'name' ], 'additionalProperties' => false ],
573 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'key' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
574 'execute_callback' => function( $input = [] ) use ( $self ) {
575 $gate = $self->confirm_gate( $input, 'Creating a component' ); if ( $gate ) { return $gate; }
576 $content = '';
577 if ( isset( $input['tree'] ) && is_array( $input['tree'] ) ) { $content = AVCF_Etch_Helpers::serialize_tree( AVCF_Etch_Helpers::sanitize_tree( $input['tree'] ) ); }
578 $id = wp_insert_post( [ 'post_type' => AVCF_Abilities_Etch_Pro::COMPONENT_CPT, 'post_title' => (string) $input['name'], 'post_excerpt' => isset( $input['description'] ) ? (string) $input['description'] : '', 'post_content' => $content, 'post_status' => 'publish' ], true );
579 if ( is_wp_error( $id ) ) { return [ 'success' => false, 'message' => $id->get_error_message() ]; }
580 $key = 'c' . substr( md5( uniqid( 'etch_component', true ) ), 0, 12 );
581 update_post_meta( $id, 'etch_component_html_key', $key );
582 update_post_meta( $id, 'etch_component_properties', isset( $input['properties'] ) && is_array( $input['properties'] ) ? $input['properties'] : [] );
583 return [ 'success' => true, 'id' => (int) $id, 'key' => $key, 'message' => 'Component created.' ];
584 },
585 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
586 'meta' => $this->write_meta( false ),
587 ] );
588
589 wp_register_ability( 'atarim/etch-edit-component', [
590 'label' => 'Edit Etch Component', 'category' => 'atarim',
591 'description' => 'SITE-WIDE. Edit a component by id (name, description, tree, and/or properties). tree replaces the block content; properties replaces the props object. Dry run unless confirm:true.',
592 'input_schema' => [ 'type' => 'object', 'properties' => [
593 'id' => [ 'type' => 'integer', 'minimum' => 1 ], 'name' => [ 'type' => 'string' ], 'description' => [ 'type' => 'string' ],
594 'tree' => [ 'type' => 'array' ], 'properties' => [ 'type' => 'object' ],
595 'confirm' => [ 'type' => 'boolean', 'default' => false ],
596 ], 'required' => [ 'id' ], 'additionalProperties' => false ],
597 'output_schema'=> $std,
598 'execute_callback' => function( $input = [] ) use ( $self ) {
599 $gate = $self->confirm_gate( $input, 'Editing a component' ); if ( $gate ) { return $gate; }
600 $id = (int) $input['id']; $p = get_post( $id );
601 if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; }
602 $up = [ 'ID' => $id ];
603 if ( isset( $input['name'] ) ) { $up['post_title'] = (string) $input['name']; }
604 if ( isset( $input['description'] ) ) { $up['post_excerpt'] = (string) $input['description']; }
605 if ( isset( $input['tree'] ) && is_array( $input['tree'] ) ) { $up['post_content'] = AVCF_Etch_Helpers::serialize_tree( AVCF_Etch_Helpers::sanitize_tree( $input['tree'] ) ); }
606 if ( count( $up ) > 1 ) { $r = wp_update_post( $up, true ); if ( is_wp_error( $r ) ) { return [ 'success' => false, 'message' => $r->get_error_message() ]; } }
607 if ( isset( $input['properties'] ) && is_array( $input['properties'] ) ) { update_post_meta( $id, 'etch_component_properties', $input['properties'] ); }
608 return [ 'success' => true, 'message' => sprintf( 'Component %d updated.', $id ) ];
609 },
610 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
611 'meta' => $this->write_meta( false ),
612 ] );
613
614 wp_register_ability( 'atarim/etch-delete-component', [
615 'label' => 'Delete Etch Component', 'category' => 'atarim',
616 'description' => 'SITE-WIDE. Delete a component by id. Dry run unless confirm:true. (Instances referencing it may break.)',
617 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1 ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
618 'output_schema'=> $std,
619 'execute_callback' => function( $input = [] ) use ( $self ) {
620 $id = (int) $input['id']; $p = get_post( $id );
621 if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; }
622 if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'message' => sprintf( 'Dry run: would delete component %d. Re-call with confirm:true.', $id ) ]; }
623 $r = wp_delete_post( $id, true );
624 return $r ? [ 'success' => true, 'message' => sprintf( 'Component %d deleted.', $id ) ] : [ 'success' => false, 'message' => 'Delete failed.' ];
625 },
626 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
627 'meta' => $this->write_meta( true ),
628 ] );
629
630 wp_register_ability( 'atarim/etch-duplicate-component', [
631 'label' => 'Duplicate Etch Component', 'category' => 'atarim',
632 'description' => 'SITE-WIDE. Duplicate a component by id (new post + new component key, title suffixed "(copy)"). Dry run unless confirm:true.',
633 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1 ], 'name' => [ 'type' => 'string', 'description' => 'Optional name for the copy.' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
634 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'key' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
635 'execute_callback' => function( $input = [] ) use ( $self ) {
636 $gate = $self->confirm_gate( $input, 'Duplicating a component' ); if ( $gate ) { return $gate; }
637 $id = (int) $input['id']; $p = get_post( $id );
638 if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; }
639 $title = isset( $input['name'] ) ? (string) $input['name'] : ( $p->post_title . ' (copy)' );
640 $new = wp_insert_post( [ 'post_type' => AVCF_Abilities_Etch_Pro::COMPONENT_CPT, 'post_title' => $title, 'post_excerpt' => $p->post_excerpt, 'post_content' => $p->post_content, 'post_status' => 'publish' ], true );
641 if ( is_wp_error( $new ) ) { return [ 'success' => false, 'message' => $new->get_error_message() ]; }
642 $key = 'c' . substr( md5( uniqid( 'etch_component', true ) ), 0, 12 );
643 update_post_meta( $new, 'etch_component_html_key', $key );
644 $props = get_post_meta( $id, 'etch_component_properties', true );
645 update_post_meta( $new, 'etch_component_properties', is_array( $props ) ? $props : [] );
646 return [ 'success' => true, 'id' => (int) $new, 'key' => $key, 'message' => 'Component duplicated.' ];
647 },
648 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
649 'meta' => $this->write_meta( false ),
650 ] );
651 }
652
653 /* ----------------------------- templates --------------------------- */
654
655 private function register_templates() {
656 $self = $this; $std = $this->std_out();
657
658 wp_register_ability( 'atarim/etch-list-templates', [
659 'label' => 'List Etch Templates', 'category' => 'atarim',
660 'description' => 'List Etch templates (WP block templates): { id, title, slug, theme }. The slug is the template-hierarchy slot (e.g. "single", "archive"); theme is the bound wp_theme term.',
661 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
662 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'templates' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
663 'execute_callback' => function( $input = [] ) use ( $self ) {
664 $posts = get_posts( [ 'post_type' => $self->template_cpt(), 'post_status' => 'any', 'numberposts' => -1 ] );
665 $out = [];
666 foreach ( $posts as $p ) {
667 $terms = wp_get_post_terms( $p->ID, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX, [ 'fields' => 'slugs' ] );
668 $out[] = [ 'id' => $p->ID, 'title' => $p->post_title, 'slug' => $p->post_name, 'theme' => ( ! is_wp_error( $terms ) && $terms ) ? $terms[0] : '' ];
669 }
670 return [ 'success' => true, 'templates' => $out, 'message' => sprintf( '%d template(s).', count( $out ) ) ];
671 },
672 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
673 'meta' => $this->ro_meta(),
674 ] );
675
676 wp_register_ability( 'atarim/etch-get-template', [
677 'label' => 'Get Etch Template', 'category' => 'atarim',
678 'description' => 'Get one template by id: title, slug, theme. (Manage the template\'s element content with the core get-content/set-content abilities using its id.)',
679 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'template_id' ], 'additionalProperties' => false ],
680 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'template' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
681 'execute_callback' => function( $input = [] ) use ( $self ) {
682 $id = (int) $input['template_id']; $p = get_post( $id );
683 if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; }
684 $terms = wp_get_post_terms( $id, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX, [ 'fields' => 'slugs' ] );
685 return [ 'success' => true, 'template' => [ 'id' => $id, 'title' => $p->post_title, 'slug' => $p->post_name, 'theme' => ( ! is_wp_error( $terms ) && $terms ) ? $terms[0] : '' ], 'message' => 'OK.' ];
686 },
687 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
688 'meta' => $this->ro_meta(),
689 ] );
690
691 wp_register_ability( 'atarim/etch-create-template', [
692 'label' => 'Create Etch Template', 'category' => 'atarim',
693 'description' => 'SITE-WIDE. Create a template. title + slug required (slug = the hierarchy slot, e.g. "single"); theme is the wp_theme term to bind to (defaults to the active theme). Element content is empty — author it via set-content with the new id. Dry run unless confirm:true.',
694 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => [ 'type' => 'string' ], 'slug' => [ 'type' => 'string' ], 'theme' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'title', 'slug' ], 'additionalProperties' => false ],
695 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
696 'execute_callback' => function( $input = [] ) use ( $self ) {
697 $gate = $self->confirm_gate( $input, 'Creating a template' ); if ( $gate ) { return $gate; }
698 $slug = sanitize_title( (string) $input['slug'] );
699 if ( $slug === '' ) { return [ 'success' => false, 'message' => 'A valid slug is required.' ]; }
700 $id = wp_insert_post( [ 'post_type' => $self->template_cpt(), 'post_title' => (string) $input['title'], 'post_name' => $slug, 'post_status' => 'publish' ], true );
701 if ( is_wp_error( $id ) ) { return [ 'success' => false, 'message' => $id->get_error_message() ]; }
702 $theme = isset( $input['theme'] ) && $input['theme'] !== '' ? (string) $input['theme'] : get_stylesheet();
703 wp_set_post_terms( $id, [ $theme ], AVCF_Abilities_Etch_Pro::TEMPLATE_TAX );
704 return [ 'success' => true, 'id' => (int) $id, 'message' => 'Template created.' ];
705 },
706 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
707 'meta' => $this->write_meta( false ),
708 ] );
709
710 wp_register_ability( 'atarim/etch-edit-template', [
711 'label' => 'Edit Etch Template', 'category' => 'atarim',
712 'description' => 'SITE-WIDE. Edit a template by id (title, slug, and/or theme binding). Dry run unless confirm:true.',
713 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'title' => [ 'type' => 'string' ], 'slug' => [ 'type' => 'string' ], 'theme' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'template_id' ], 'additionalProperties' => false ],
714 'output_schema'=> $std,
715 'execute_callback' => function( $input = [] ) use ( $self ) {
716 $gate = $self->confirm_gate( $input, 'Editing a template' ); if ( $gate ) { return $gate; }
717 $id = (int) $input['template_id']; $p = get_post( $id );
718 if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; }
719 $up = [ 'ID' => $id ];
720 if ( isset( $input['title'] ) ) { $up['post_title'] = (string) $input['title']; }
721 if ( isset( $input['slug'] ) ) { $up['post_name'] = sanitize_title( (string) $input['slug'] ); }
722 if ( count( $up ) > 1 ) { $r = wp_update_post( $up, true ); if ( is_wp_error( $r ) ) { return [ 'success' => false, 'message' => $r->get_error_message() ]; } }
723 if ( isset( $input['theme'] ) ) { wp_set_post_terms( $id, [ (string) $input['theme'] ], AVCF_Abilities_Etch_Pro::TEMPLATE_TAX ); }
724 return [ 'success' => true, 'message' => sprintf( 'Template %d updated.', $id ) ];
725 },
726 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
727 'meta' => $this->write_meta( false ),
728 ] );
729
730 wp_register_ability( 'atarim/etch-delete-template', [
731 'label' => 'Delete Etch Template', 'category' => 'atarim',
732 'description' => 'SITE-WIDE. Delete a template by id. Dry run unless confirm:true.',
733 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'template_id' ], 'additionalProperties' => false ],
734 'output_schema'=> $std,
735 'execute_callback' => function( $input = [] ) use ( $self ) {
736 $id = (int) $input['template_id']; $p = get_post( $id );
737 if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; }
738 if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'message' => sprintf( 'Dry run: would delete template %d. Re-call with confirm:true.', $id ) ]; }
739 $r = wp_delete_post( $id, true );
740 return $r ? [ 'success' => true, 'message' => sprintf( 'Template %d deleted.', $id ) ] : [ 'success' => false, 'message' => 'Delete failed.' ];
741 },
742 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
743 'meta' => $this->write_meta( true ),
744 ] );
745
746 wp_register_ability( 'atarim/etch-duplicate-template', [
747 'label' => 'Duplicate Etch Template', 'category' => 'atarim',
748 'description' => 'SITE-WIDE. Duplicate a template by id (copies content + theme binding; new slug required to avoid a hierarchy clash). Dry run unless confirm:true.',
749 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'slug' => [ 'type' => 'string' ], 'title' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'template_id', 'slug' ], 'additionalProperties' => false ],
750 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
751 'execute_callback' => function( $input = [] ) use ( $self ) {
752 $gate = $self->confirm_gate( $input, 'Duplicating a template' ); if ( $gate ) { return $gate; }
753 $id = (int) $input['template_id']; $p = get_post( $id );
754 if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; }
755 $slug = sanitize_title( (string) $input['slug'] );
756 if ( $slug === '' ) { return [ 'success' => false, 'message' => 'A valid slug is required.' ]; }
757 $new = wp_insert_post( [ 'post_type' => $self->template_cpt(), 'post_title' => isset( $input['title'] ) ? (string) $input['title'] : ( $p->post_title . ' (copy)' ), 'post_name' => $slug, 'post_content' => $p->post_content, 'post_status' => 'publish' ], true );
758 if ( is_wp_error( $new ) ) { return [ 'success' => false, 'message' => $new->get_error_message() ]; }
759 $terms = wp_get_post_terms( $id, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX, [ 'fields' => 'slugs' ] );
760 if ( ! is_wp_error( $terms ) && $terms ) { wp_set_post_terms( $new, $terms, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX ); }
761 return [ 'success' => true, 'id' => (int) $new, 'message' => 'Template duplicated.' ];
762 },
763 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
764 'meta' => $this->write_meta( false ),
765 ] );
766 }
767
768 /* ------------------------- styles / stylesheets -------------------- */
769
770 private function register_styles() {
771 $self = $this; $std = $this->std_out();
772
773 wp_register_ability( 'atarim/etch-get-styles', [
774 'label' => 'Get Etch Styles', 'category' => 'atarim',
775 'description' => 'Read the Etch styles layer (the etch_styles option) — the design tokens/variables layer. Returns the raw structure (empty is normal before any styles are saved).',
776 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
777 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'styles' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
778 'execute_callback' => function( $input = [] ) use ( $self ) {
779 return [ 'success' => true, 'styles' => $self->opt_get( AVCF_Abilities_Etch_Pro::O_STYLES ), 'message' => 'OK.' ];
780 },
781 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
782 'meta' => $this->ro_meta(),
783 ] );
784
785 wp_register_ability( 'atarim/etch-list-stylesheets', [
786 'label' => 'List Etch Stylesheets', 'category' => 'atarim',
787 'description' => 'List Etch global stylesheets (etch_global_stylesheets): { id, name }. Fetch CSS with get-stylesheet.',
788 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
789 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stylesheets' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
790 'execute_callback' => function( $input = [] ) use ( $self ) {
791 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS );
792 $out = [];
793 foreach ( $map as $id => $s ) { if ( is_array( $s ) ) { $out[] = [ 'id' => (string) $id, 'name' => $s['name'] ?? '' ]; } }
794 return [ 'success' => true, 'stylesheets' => $out, 'message' => sprintf( '%d stylesheet(s).', count( $out ) ) ];
795 },
796 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
797 'meta' => $this->ro_meta(),
798 ] );
799
800 wp_register_ability( 'atarim/etch-get-stylesheet', [
801 'label' => 'Get Etch Stylesheet', 'category' => 'atarim',
802 'description' => 'Get a global stylesheet by id: { id, name, css }.',
803 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
804 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stylesheet' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
805 'execute_callback' => function( $input = [] ) use ( $self ) {
806 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); $id = (string) $input['id'];
807 if ( ! isset( $map[ $id ] ) || ! is_array( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Stylesheet "%s" not found.', $id ) ]; }
808 return [ 'success' => true, 'stylesheet' => [ 'id' => $id, 'name' => $map[ $id ]['name'] ?? '', 'css' => $map[ $id ]['css'] ?? '' ], 'message' => 'OK.' ];
809 },
810 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
811 'meta' => $this->ro_meta(),
812 ] );
813
814 wp_register_ability( 'atarim/etch-create-stylesheet', [
815 'label' => 'Create Etch Stylesheet', 'category' => 'atarim',
816 'description' => 'SITE-WIDE. Create a global stylesheet. name required; css is the stylesheet body. Dry run unless confirm:true.',
817 'input_schema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string' ], 'css' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'name' ], 'additionalProperties' => false ],
818 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
819 'execute_callback' => function( $input = [] ) use ( $self ) {
820 $gate = $self->confirm_gate( $input, 'Creating a stylesheet' ); if ( $gate ) { return $gate; }
821 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS );
822 $id = $self->gen_id();
823 $map[ $id ] = [ 'name' => (string) $input['name'], 'css' => isset( $input['css'] ) ? (string) $input['css'] : '' ];
824 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_SHEETS, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
825 return [ 'success' => true, 'id' => $id, 'message' => 'Stylesheet created.' ];
826 },
827 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
828 'meta' => $this->write_meta( false ),
829 ] );
830
831 wp_register_ability( 'atarim/etch-edit-stylesheet', [
832 'label' => 'Edit Etch Stylesheet', 'category' => 'atarim',
833 'description' => 'SITE-WIDE. Edit a stylesheet by id (name and/or css). Dry run unless confirm:true.',
834 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'name' => [ 'type' => 'string' ], 'css' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
835 'output_schema'=> $std,
836 'execute_callback' => function( $input = [] ) use ( $self ) {
837 $gate = $self->confirm_gate( $input, 'Editing a stylesheet' ); if ( $gate ) { return $gate; }
838 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); $id = (string) $input['id'];
839 if ( ! isset( $map[ $id ] ) || ! is_array( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Stylesheet "%s" not found.', $id ) ]; }
840 if ( isset( $input['name'] ) ) { $map[ $id ]['name'] = (string) $input['name']; }
841 if ( isset( $input['css'] ) ) { $map[ $id ]['css'] = (string) $input['css']; }
842 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_SHEETS, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
843 return [ 'success' => true, 'message' => sprintf( 'Stylesheet "%s" updated.', $id ) ];
844 },
845 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
846 'meta' => $this->write_meta( false ),
847 ] );
848
849 wp_register_ability( 'atarim/etch-delete-stylesheet', [
850 'label' => 'Delete Etch Stylesheet', 'category' => 'atarim',
851 'description' => 'SITE-WIDE. Delete a stylesheet by id. Dry run unless confirm:true.',
852 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
853 'output_schema'=> $std,
854 'execute_callback' => function( $input = [] ) use ( $self ) {
855 $gate = $self->confirm_gate( $input, 'Deleting a stylesheet' ); if ( $gate ) { return $gate; }
856 $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); $id = (string) $input['id'];
857 if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Stylesheet "%s" not found.', $id ) ]; }
858 unset( $map[ $id ] );
859 if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_SHEETS, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
860 return [ 'success' => true, 'message' => sprintf( 'Stylesheet "%s" deleted.', $id ) ];
861 },
862 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
863 'meta' => $this->write_meta( true ),
864 ] );
865 }
866 }
867