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 / third-party / page-builder / elementor / class-avcf-abilities-elementor-pro.php
atarim-visual-collaboration / third-party / page-builder / elementor Last commit date
class-avcf-abilities-elementor-pro.php 4 weeks ago class-avcf-abilities-elementor.php 4 weeks ago class-avcf-elementor-detector.php 4 weeks ago class-avcf-elementor-helpers.php 4 weeks ago
class-avcf-abilities-elementor-pro.php
754 lines
1 <?php
2 /**
3 * Elementor — Advanced / Pro MCP abilities.
4 *
5 * EXPERIMENTAL. These sit on Elementor's INTERNAL, non-public APIs (the global
6 * classes repository, the active Kit's v3 global colors/typography, the
7 * Variables repository, the dynamic-tags Manager, atomic widgets) which change
8 * between Elementor releases. They could not be tested here against a live
9 * Elementor (Pro). Every callback is area-gated on the specific class being
10 * present and wrapped in try/catch so an unavailable or changed API returns a
11 * clean error rather than fataling or corrupting data. The site-wide writers
12 * (global classes, v3 styles, variables) are dry-run unless confirm:true,
13 * because they affect every page on the site. VALIDATE ON A LIVE ELEMENTOR PRO
14 * INSTALL BEFORE FLEET ROLLOUT.
15 *
16 * Areas (atarim/elementor-*):
17 * global classes (5), v3 styles (7), variables (4), dynamic tags (3),
18 * interactions (3), delete-element-style (1), create-atomic-widget (1),
19 * validate-widget (1) = 25 abilities.
20 *
21 * @package atarim-visual-collaboration
22 */
23
24 if ( ! defined('ABSPATH') ) {
25 exit;
26 }
27
28 class AVCF_Abilities_Elementor_Pro extends AVCF_Abilities_Base {
29
30 /** @var AVCF_Elementor_Detector */
31 private $detector;
32
33 public function __construct() {
34 $this->detector = new AVCF_Elementor_Detector();
35 }
36
37 public function register() {
38 if ( ! $this->detector->avcf_elementor_is_available() ) {
39 return;
40 }
41 $this->register_global_classes();
42 $this->register_v3_styles();
43 $this->register_variables();
44 $this->register_dynamic_tags();
45 $this->register_interactions();
46 $this->register_element_style();
47 $this->register_atomic_widget();
48 $this->register_validate_widget();
49 }
50
51 /* ----------------------------- shared ------------------------------ */
52
53 private function ro_meta() {
54 return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ] ];
55 }
56 private function write_meta( $destructive = false ) {
57 return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => false, 'destructive' => (bool) $destructive, 'idempotent' => false ] ];
58 }
59 private function site_can() { return current_user_can( 'manage_options' ); }
60 private function post_can( $post_id ) { return current_user_can( 'edit_post', (int) $post_id ) || current_user_can( 'edit_posts' ); }
61
62 private function active_kit() {
63 if ( ! class_exists( '\Elementor\Plugin' ) ) {
64 return null;
65 }
66 $p = \Elementor\Plugin::$instance;
67 if ( isset( $p->kits_manager ) && is_object( $p->kits_manager ) && method_exists( $p->kits_manager, 'get_active_kit' ) ) {
68 return $p->kits_manager->get_active_kit();
69 }
70 return null;
71 }
72
73 private function gen_id() {
74 return substr( md5( uniqid( (string) wp_rand(), true ) ), 0, 7 );
75 }
76
77 /** Standard "this is a site-wide change" confirm gate. */
78 private function confirm_gate( $input, $what ) {
79 if ( empty( $input['confirm'] ) ) {
80 return [ 'success' => true, 'applied' => false, 'message' => sprintf( 'Dry run: %s affects every page on the site. Re-call with confirm:true to apply.', $what ) ];
81 }
82 return null;
83 }
84
85 /* ----------------------- global classes (5) ----------------------- */
86
87 private function register_global_classes() {
88 $self = $this;
89 $repo_class = '\Elementor\Modules\GlobalClasses\Global_Classes_Repository';
90
91 $guard = function() use ( $repo_class ) {
92 if ( ! class_exists( $repo_class ) ) {
93 return [ 'success' => false, 'message' => 'Elementor global classes are not available on this install (feature/version missing).' ];
94 }
95 return null;
96 };
97
98 wp_register_ability( 'atarim/elementor-list-global-classes', [
99 'label' => 'List Elementor Global Classes', 'category' => 'atarim',
100 'description' => 'EXPERIMENTAL. List Elementor global CSS classes (id, label) and their order from the global classes repository.',
101 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
102 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'classes' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
103 'execute_callback' => function( $input = [] ) use ( $guard, $repo_class ) {
104 $g = $guard(); if ( $g ) { return $g; }
105 try {
106 $repo = $repo_class::make();
107 $all = $repo->all();
108 $items = method_exists( $all, 'get_items' ) ? (array) $all->get_items()->all() : [];
109 $order = method_exists( $all, 'get_order' ) ? (array) $all->get_order()->all() : [];
110 $out = [];
111 foreach ( $items as $id => $def ) {
112 $out[] = [ 'id' => (string) $id, 'label' => isset( $def['label'] ) ? $def['label'] : ( is_array( $def ) && isset( $def['id'] ) ? $def['id'] : (string) $id ) ];
113 }
114 return [ 'success' => true, 'classes' => $out, 'order' => array_values( $order ), 'message' => 'OK.' ];
115 } catch ( \Throwable $e ) {
116 return [ 'success' => false, 'message' => 'Failed to read global classes: ' . $e->getMessage() ];
117 }
118 },
119 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
120 'meta' => $this->ro_meta(),
121 ] );
122
123 // create / edit / delete share a put-based mutation; the put signature
124 // varies by Elementor version, so these are best-effort + confirm-gated.
125 $mutate = function( $op ) use ( $self, $guard, $repo_class ) {
126 return function( $input = [] ) use ( $self, $guard, $repo_class, $op ) {
127 $g = $guard(); if ( $g ) { return $g; }
128 $gate = $self->confirm_gate( $input, sprintf( 'Global class %s', $op ) );
129 if ( $gate ) { return $gate; }
130 try {
131 $repo = $repo_class::make();
132 $all = $repo->all();
133 $items = method_exists( $all, 'get_items' ) ? (array) $all->get_items()->all() : [];
134 $order = method_exists( $all, 'get_order' ) ? (array) $all->get_order()->all() : [];
135 if ( $op === 'create' ) {
136 $id = 'g-' . $self->gen_id();
137 $items[ $id ] = [ 'id' => $id, 'label' => isset( $input['label'] ) ? (string) $input['label'] : $id, 'type' => 'class', 'variants' => isset( $input['variants'] ) && is_array( $input['variants'] ) ? $input['variants'] : [] ];
138 $order[] = $id;
139 } elseif ( $op === 'edit' ) {
140 $id = isset( $input['id'] ) ? (string) $input['id'] : '';
141 if ( ! isset( $items[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Global class "%s" not found.', $id ) ]; }
142 if ( isset( $input['label'] ) ) { $items[ $id ]['label'] = (string) $input['label']; }
143 if ( isset( $input['variants'] ) ) { $items[ $id ]['variants'] = (array) $input['variants']; }
144 } else { // delete
145 $id = isset( $input['id'] ) ? (string) $input['id'] : '';
146 if ( ! isset( $items[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Global class "%s" not found.', $id ) ]; }
147 unset( $items[ $id ] );
148 $order = array_values( array_filter( $order, function( $o ) use ( $id ) { return $o !== $id; } ) );
149 }
150 if ( ! method_exists( $repo, 'put' ) ) {
151 return [ 'success' => false, 'message' => 'This Elementor version does not expose Global_Classes_Repository::put(); the write path needs live-Pro validation.' ];
152 }
153 $repo->put( $items, $order );
154 AVCF_Elementor_Helpers::clear_css_cache( 0 );
155 return [ 'success' => true, 'applied' => true, 'id' => isset( $id ) ? $id : '', 'message' => sprintf( 'Global class %s applied.', $op ) ];
156 } catch ( \Throwable $e ) {
157 return [ 'success' => false, 'message' => ucfirst( $op ) . ' failed: ' . $e->getMessage() ];
158 }
159 };
160 };
161
162 wp_register_ability( 'atarim/elementor-create-global-class', [
163 'label' => 'Create Elementor Global Class', 'category' => 'atarim',
164 'description' => 'EXPERIMENTAL, SITE-WIDE. Create a global CSS class. variants is the Elementor variant/props structure. Dry run unless confirm:true.',
165 'input_schema' => [ 'type' => 'object', 'properties' => [ 'label' => [ 'type' => 'string' ], 'variants' => [ 'type' => 'array' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'label' ], 'additionalProperties' => false ],
166 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
167 'execute_callback' => $mutate( 'create' ),
168 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
169 'meta' => $this->write_meta( false ),
170 ] );
171 wp_register_ability( 'atarim/elementor-edit-global-class', [
172 'label' => 'Edit Elementor Global Class', 'category' => 'atarim',
173 'description' => 'EXPERIMENTAL, SITE-WIDE. Edit a global CSS class by id (label and/or variants). Dry run unless confirm:true.',
174 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'label' => [ 'type' => 'string' ], 'variants' => [ 'type' => 'array' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
175 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
176 'execute_callback' => $mutate( 'edit' ),
177 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
178 'meta' => $this->write_meta( false ),
179 ] );
180 wp_register_ability( 'atarim/elementor-delete-global-class', [
181 'label' => 'Delete Elementor Global Class', 'category' => 'atarim',
182 'description' => 'EXPERIMENTAL, SITE-WIDE. Delete a global CSS class by id. Dry run unless confirm:true.',
183 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
184 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
185 'execute_callback' => $mutate( 'delete' ),
186 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
187 'meta' => $this->write_meta( true ),
188 ] );
189
190 wp_register_ability( 'atarim/elementor-apply-global-class', [
191 'label' => 'Apply Elementor Global Class', 'category' => 'atarim',
192 'description' => 'EXPERIMENTAL. Apply (or remove) a global class on one element of a post by adding/removing the class id in the element\'s "classes" setting. Per-post, not site-wide.',
193 'input_schema' => [ 'type' => 'object', 'properties' => [
194 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'element_id' => [ 'type' => 'string' ], 'class_id' => [ 'type' => 'string' ], 'remove' => [ 'type' => 'boolean', 'default' => false ],
195 ], 'required' => [ 'post_id', 'element_id', 'class_id' ], 'additionalProperties' => false ],
196 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
197 'execute_callback' => function( $input = [] ) use ( $self ) {
198 $post_id = isset( $input['post_id'] ) ? (int) $input['post_id'] : 0;
199 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
200 if ( ! $self->post_can( $post_id ) ) { return [ 'success' => false, 'message' => 'No permission to edit this post.' ]; }
201 $eid = (string) $input['element_id']; $cid = (string) $input['class_id'];
202 $tree = AVCF_Elementor_Helpers::read_tree( $post_id ); $found = false;
203 $tree = AVCF_Elementor_Helpers::map_edit( $tree, $eid, function( $node ) use ( $cid, $input ) {
204 $settings = isset( $node['settings'] ) && is_array( $node['settings'] ) ? $node['settings'] : [];
205 $classes = isset( $settings['classes']['value'] ) && is_array( $settings['classes']['value'] ) ? $settings['classes']['value'] : [];
206 if ( ! empty( $input['remove'] ) ) {
207 $classes = array_values( array_filter( $classes, function( $c ) use ( $cid ) { return $c !== $cid; } ) );
208 } elseif ( ! in_array( $cid, $classes, true ) ) {
209 $classes[] = $cid;
210 }
211 $settings['classes'] = [ '$$type' => 'classes', 'value' => $classes ];
212 $node['settings'] = $settings;
213 return $node;
214 }, $found );
215 if ( ! $found ) { return [ 'success' => false, 'message' => sprintf( 'Element "%s" not found.', $eid ) ]; }
216 if ( ! AVCF_Elementor_Helpers::write_tree( $post_id, $tree ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
217 return [ 'success' => true, 'message' => ! empty( $input['remove'] ) ? 'Class removed from element.' : 'Class applied to element.' ];
218 },
219 'permission_callback' => function() use ( $self ) { return current_user_can( 'edit_posts' ); },
220 'meta' => $this->write_meta( false ),
221 ] );
222 }
223
224 /* -------------------------- v3 styles (7) -------------------------- */
225
226 private function register_v3_styles() {
227 $self = $this;
228
229 wp_register_ability( 'atarim/elementor-list-v3-styles', [
230 'label' => 'List Elementor v3 Global Styles', 'category' => 'atarim',
231 'description' => 'List the active Kit\'s v3 global colors and typography: system_colors, custom_colors, system_typography, custom_typography (each with _id and title).',
232 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
233 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'styles' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
234 'execute_callback' => function( $input = [] ) use ( $self ) {
235 $kit = $self->active_kit();
236 if ( ! $kit ) { return [ 'success' => false, 'message' => 'No active Elementor Kit found.' ]; }
237 try {
238 return [ 'success' => true, 'styles' => [
239 'system_colors' => (array) ( $kit->get_settings( 'system_colors' ) ?: [] ),
240 'custom_colors' => (array) ( $kit->get_settings( 'custom_colors' ) ?: [] ),
241 'system_typography' => (array) ( $kit->get_settings( 'system_typography' ) ?: [] ),
242 'custom_typography' => (array) ( $kit->get_settings( 'custom_typography' ) ?: [] ),
243 ], 'message' => 'OK.' ];
244 } catch ( \Throwable $e ) {
245 return [ 'success' => false, 'message' => 'Failed to read kit styles: ' . $e->getMessage() ];
246 }
247 },
248 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
249 'meta' => $this->ro_meta(),
250 ] );
251
252 // Shared kit-array mutation for custom_colors / custom_typography.
253 $mutate = function( $setting_key, $op ) use ( $self ) {
254 return function( $input = [] ) use ( $self, $setting_key, $op ) {
255 $kit = $self->active_kit();
256 if ( ! $kit ) { return [ 'success' => false, 'message' => 'No active Elementor Kit found.' ]; }
257 $gate = $self->confirm_gate( $input, sprintf( '%s %s', $setting_key, $op ) );
258 if ( $gate ) { return $gate; }
259 try {
260 $list = (array) ( $kit->get_settings( $setting_key ) ?: [] );
261 if ( $op === 'create' ) {
262 $entry = [ '_id' => $self->gen_id(), 'title' => isset( $input['title'] ) ? (string) $input['title'] : 'Untitled' ];
263 if ( $setting_key === 'custom_colors' ) {
264 $entry['color'] = isset( $input['color'] ) ? (string) $input['color'] : '#000000';
265 } else {
266 $entry['typography_typography'] = 'custom';
267 if ( isset( $input['font_family'] ) ) { $entry['typography_font_family'] = (string) $input['font_family']; }
268 if ( isset( $input['font_weight'] ) ) { $entry['typography_font_weight'] = (string) $input['font_weight']; }
269 if ( isset( $input['font_size'] ) ) { $entry['typography_font_size'] = [ 'unit' => 'px', 'size' => (float) $input['font_size'] ]; }
270 }
271 $list[] = $entry;
272 $new_id = $entry['_id'];
273 } else {
274 $id = isset( $input['id'] ) ? (string) $input['id'] : '';
275 $hit = false;
276 foreach ( $list as $i => $e ) {
277 if ( isset( $e['_id'] ) && (string) $e['_id'] === $id ) {
278 $hit = true;
279 if ( $op === 'delete' ) {
280 unset( $list[ $i ] );
281 } else { // edit
282 if ( isset( $input['title'] ) ) { $list[ $i ]['title'] = (string) $input['title']; }
283 if ( $setting_key === 'custom_colors' && isset( $input['color'] ) ) { $list[ $i ]['color'] = (string) $input['color']; }
284 if ( $setting_key === 'custom_typography' ) {
285 if ( isset( $input['font_family'] ) ) { $list[ $i ]['typography_font_family'] = (string) $input['font_family']; }
286 if ( isset( $input['font_weight'] ) ) { $list[ $i ]['typography_font_weight'] = (string) $input['font_weight']; }
287 if ( isset( $input['font_size'] ) ) { $list[ $i ]['typography_font_size'] = [ 'unit' => 'px', 'size' => (float) $input['font_size'] ]; }
288 }
289 }
290 break;
291 }
292 }
293 if ( ! $hit ) { return [ 'success' => false, 'message' => sprintf( 'Style "%s" not found.', $id ) ]; }
294 $list = array_values( $list );
295 }
296 $kit->update_settings( [ $setting_key => $list ] );
297 if ( method_exists( $kit, 'get_id' ) ) { AVCF_Elementor_Helpers::clear_css_cache( (int) $kit->get_id() ); }
298 return [ 'success' => true, 'applied' => true, 'id' => isset( $new_id ) ? $new_id : ( isset( $id ) ? $id : '' ), 'message' => sprintf( '%s %s applied.', $setting_key, $op ) ];
299 } catch ( \Throwable $e ) {
300 return [ 'success' => false, 'message' => ucfirst( $op ) . ' failed: ' . $e->getMessage() ];
301 }
302 };
303 };
304
305 $color_props = [ 'title' => [ 'type' => 'string' ], 'color' => [ 'type' => 'string', 'description' => 'Hex color, e.g. "#3366ff".' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ];
306 $typo_props = [ 'title' => [ 'type' => 'string' ], 'font_family' => [ 'type' => 'string' ], 'font_weight' => [ 'type' => 'string' ], 'font_size' => [ 'type' => 'number' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ];
307 $std_out = [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ];
308
309 foreach ( [
310 [ 'atarim/elementor-create-v3-color', 'Create v3 Global Color', 'custom_colors', 'create', array_merge( $color_props, [] ), [ 'title', 'color' ] ],
311 [ 'atarim/elementor-edit-v3-color', 'Edit v3 Global Color', 'custom_colors', 'edit', array_merge( [ 'id' => [ 'type' => 'string' ] ], $color_props ), [ 'id' ] ],
312 [ 'atarim/elementor-delete-v3-color', 'Delete v3 Global Color', 'custom_colors', 'delete', [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], [ 'id' ] ],
313 [ 'atarim/elementor-create-v3-typography', 'Create v3 Global Typography', 'custom_typography', 'create', array_merge( $typo_props, [] ), [ 'title' ] ],
314 [ 'atarim/elementor-edit-v3-typography', 'Edit v3 Global Typography', 'custom_typography', 'edit', array_merge( [ 'id' => [ 'type' => 'string' ] ], $typo_props ), [ 'id' ] ],
315 [ 'atarim/elementor-delete-v3-typography', 'Delete v3 Global Typography', 'custom_typography', 'delete', [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], [ 'id' ] ],
316 ] as $def ) {
317 list( $name, $label, $key, $op, $props, $required ) = $def;
318 wp_register_ability( $name, [
319 'label' => $label, 'category' => 'atarim',
320 'description' => sprintf( 'SITE-WIDE. %s in the active Kit. Dry run unless confirm:true.', $label ),
321 'input_schema' => [ 'type' => 'object', 'properties' => $props, 'required' => $required, 'additionalProperties' => false ],
322 'output_schema'=> $std_out,
323 'execute_callback' => $mutate( $key, $op ),
324 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
325 'meta' => $this->write_meta( $op === 'delete' ),
326 ] );
327 }
328 }
329
330 /* -------------------------- variables (4) -------------------------- */
331
332 private function register_variables() {
333 $self = $this;
334 $repo_class = '\Elementor\Modules\Variables\Storage\Variables_Repository';
335
336 $guard = function() use ( $repo_class ) {
337 if ( ! class_exists( $repo_class ) ) {
338 return [ 'success' => false, 'message' => 'Elementor Variables are not available on this install (feature/version missing).' ];
339 }
340 return null;
341 };
342
343 wp_register_ability( 'atarim/elementor-list-variables', [
344 'label' => 'List Elementor Variables', 'category' => 'atarim',
345 'description' => 'EXPERIMENTAL. List Elementor global Variables (id, label, type, value) from the active Kit\'s variables repository.',
346 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
347 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'variables' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
348 'execute_callback' => function( $input = [] ) use ( $self, $guard, $repo_class ) {
349 $g = $guard(); if ( $g ) { return $g; }
350 $kit = $self->active_kit();
351 if ( ! $kit ) { return [ 'success' => false, 'message' => 'No active Elementor Kit found.' ]; }
352 try {
353 $repo = new $repo_class( $kit );
354 $collection = $repo->load();
355 $out = [];
356 $list = method_exists( $collection, 'all' ) ? $collection->all() : ( is_iterable( $collection ) ? $collection : [] );
357 foreach ( $list as $var ) {
358 if ( is_object( $var ) ) {
359 $out[] = [
360 'id' => method_exists( $var, 'id' ) ? $var->id() : '',
361 'label' => method_exists( $var, 'label' ) ? $var->label() : '',
362 'type' => method_exists( $var, 'type' ) ? $var->type() : '',
363 'value' => method_exists( $var, 'value' ) ? $var->value() : null,
364 ];
365 } elseif ( is_array( $var ) ) {
366 $out[] = $var;
367 }
368 }
369 return [ 'success' => true, 'variables' => $out, 'message' => 'OK.' ];
370 } catch ( \Throwable $e ) {
371 return [ 'success' => false, 'message' => 'Failed to read variables: ' . $e->getMessage() ];
372 }
373 },
374 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
375 'meta' => $this->ro_meta(),
376 ] );
377
378 // create / edit / delete: the Variables collection mutation API
379 // (create_new / soft_delete / save) varies; best-effort + confirm-gated.
380 $note = 'EXPERIMENTAL, SITE-WIDE. The Variables write API varies by Elementor version; this is best-effort and may need live-Pro adjustment. Dry run unless confirm:true.';
381 $std_out = [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ];
382
383 wp_register_ability( 'atarim/elementor-create-variable', [
384 'label' => 'Create Elementor Variable', 'category' => 'atarim', 'description' => $note,
385 'input_schema' => [ 'type' => 'object', 'properties' => [ 'label' => [ 'type' => 'string' ], 'type' => [ 'type' => 'string', 'description' => 'e.g. color or font.' ], 'value' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'label', 'type', 'value' ], 'additionalProperties' => false ],
386 'output_schema'=> $std_out,
387 'execute_callback' => function( $input = [] ) use ( $self, $guard, $repo_class ) {
388 $g = $guard(); if ( $g ) { return $g; }
389 $gate = $self->confirm_gate( $input, 'Variable create' ); if ( $gate ) { return $gate; }
390 $kit = $self->active_kit(); if ( ! $kit ) { return [ 'success' => false, 'message' => 'No active Kit.' ]; }
391 try {
392 $repo = new $repo_class( $kit );
393 $collection = $repo->load();
394 if ( ! method_exists( $collection, 'create_new' ) && ! method_exists( $collection, 'add' ) ) {
395 return [ 'success' => false, 'message' => 'This Elementor version\'s Variables collection has no create method exposed; needs live-Pro validation.' ];
396 }
397 if ( method_exists( $collection, 'create_new' ) ) {
398 $collection->create_new( [ 'label' => (string) $input['label'], 'type' => (string) $input['type'], 'value' => (string) $input['value'] ] );
399 } else {
400 $collection->add( [ 'label' => (string) $input['label'], 'type' => (string) $input['type'], 'value' => (string) $input['value'] ] );
401 }
402 $repo->save( $collection );
403 if ( method_exists( $kit, 'get_id' ) ) { AVCF_Elementor_Helpers::clear_css_cache( (int) $kit->get_id() ); }
404 return [ 'success' => true, 'applied' => true, 'message' => 'Variable created.' ];
405 } catch ( \Throwable $e ) {
406 return [ 'success' => false, 'message' => 'Create failed: ' . $e->getMessage() ];
407 }
408 },
409 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
410 'meta' => $this->write_meta( false ),
411 ] );
412
413 wp_register_ability( 'atarim/elementor-edit-variable', [
414 'label' => 'Edit Elementor Variable', 'category' => 'atarim', 'description' => $note,
415 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'label' => [ 'type' => 'string' ], 'value' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
416 'output_schema'=> $std_out,
417 'execute_callback' => function( $input = [] ) use ( $self, $guard, $repo_class ) {
418 $g = $guard(); if ( $g ) { return $g; }
419 $gate = $self->confirm_gate( $input, 'Variable edit' ); if ( $gate ) { return $gate; }
420 $kit = $self->active_kit(); if ( ! $kit ) { return [ 'success' => false, 'message' => 'No active Kit.' ]; }
421 try {
422 $repo = new $repo_class( $kit );
423 $collection = $repo->load();
424 return [ 'success' => false, 'message' => 'Variable edit is not yet wired to a stable API on this build — read with list-variables and adjust against live Elementor Pro. (id=' . (string) $input['id'] . ')' ];
425 } catch ( \Throwable $e ) {
426 return [ 'success' => false, 'message' => 'Edit failed: ' . $e->getMessage() ];
427 }
428 },
429 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
430 'meta' => $this->write_meta( false ),
431 ] );
432
433 wp_register_ability( 'atarim/elementor-delete-variable', [
434 'label' => 'Delete Elementor Variable', 'category' => 'atarim', 'description' => $note,
435 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ],
436 'output_schema'=> $std_out,
437 'execute_callback' => function( $input = [] ) use ( $self, $guard, $repo_class ) {
438 $g = $guard(); if ( $g ) { return $g; }
439 $gate = $self->confirm_gate( $input, 'Variable delete' ); if ( $gate ) { return $gate; }
440 $kit = $self->active_kit(); if ( ! $kit ) { return [ 'success' => false, 'message' => 'No active Kit.' ]; }
441 try {
442 $repo = new $repo_class( $kit );
443 $collection = $repo->load();
444 $id = (string) $input['id'];
445 $var = method_exists( $collection, 'get' ) ? $collection->get( $id ) : null;
446 if ( $var && method_exists( $var, 'soft_delete' ) ) {
447 $var->soft_delete();
448 $repo->save( $collection );
449 if ( method_exists( $kit, 'get_id' ) ) { AVCF_Elementor_Helpers::clear_css_cache( (int) $kit->get_id() ); }
450 return [ 'success' => true, 'applied' => true, 'message' => 'Variable deleted.' ];
451 }
452 return [ 'success' => false, 'message' => 'Could not resolve the variable for deletion on this build; needs live-Pro validation.' ];
453 } catch ( \Throwable $e ) {
454 return [ 'success' => false, 'message' => 'Delete failed: ' . $e->getMessage() ];
455 }
456 },
457 'permission_callback' => function() use ( $self ) { return $self->site_can(); },
458 'meta' => $this->write_meta( true ),
459 ] );
460 }
461
462 /* ------------------------ dynamic tags (3) ------------------------- */
463
464 private function register_dynamic_tags() {
465 $self = $this;
466 $detector = $this->detector;
467
468 $guard = function() use ( $detector ) {
469 if ( ! $detector->avcf_elementor_is_pro() ) {
470 return [ 'success' => false, 'message' => 'Dynamic tags require Elementor Pro, which is not active.' ];
471 }
472 return null;
473 };
474 $manager = function() {
475 $p = \Elementor\Plugin::$instance;
476 return isset( $p->dynamic_tags ) ? $p->dynamic_tags : null;
477 };
478
479 wp_register_ability( 'atarim/elementor-list-dynamic-tags', [
480 'label' => 'List Elementor Dynamic Tags', 'category' => 'atarim',
481 'description' => 'List the registered Elementor dynamic tags (name, title, group). Requires Elementor Pro.',
482 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ],
483 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'tags' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
484 'execute_callback' => function( $input = [] ) use ( $guard, $manager ) {
485 $g = $guard(); if ( $g ) { return $g; }
486 $m = $manager(); if ( ! is_object( $m ) ) { return [ 'success' => false, 'message' => 'Dynamic tags manager unavailable.' ]; }
487 try {
488 $tags = method_exists( $m, 'get_tags_config' ) ? (array) $m->get_tags_config() : [];
489 $out = [];
490 foreach ( $tags as $name => $cfg ) {
491 $out[] = [ 'name' => (string) $name, 'title' => isset( $cfg['title'] ) ? $cfg['title'] : '', 'group' => isset( $cfg['group'] ) ? $cfg['group'] : '' ];
492 }
493 return [ 'success' => true, 'tags' => $out, 'message' => 'OK.' ];
494 } catch ( \Throwable $e ) {
495 return [ 'success' => false, 'message' => 'Failed to list dynamic tags: ' . $e->getMessage() ];
496 }
497 },
498 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
499 'meta' => $this->ro_meta(),
500 ] );
501
502 wp_register_ability( 'atarim/elementor-get-dynamic-tag', [
503 'label' => 'Get Elementor Dynamic Tag', 'category' => 'atarim',
504 'description' => 'Get one dynamic tag\'s config/controls by name. Requires Elementor Pro.',
505 'input_schema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string' ] ], 'required' => [ 'name' ], 'additionalProperties' => false ],
506 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'tag' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
507 'execute_callback' => function( $input = [] ) use ( $guard, $manager ) {
508 $g = $guard(); if ( $g ) { return $g; }
509 $m = $manager(); if ( ! is_object( $m ) ) { return [ 'success' => false, 'message' => 'Dynamic tags manager unavailable.' ]; }
510 try {
511 $info = method_exists( $m, 'get_tag_info' ) ? $m->get_tag_info( (string) $input['name'] ) : null;
512 if ( ! $info ) { return [ 'success' => false, 'message' => sprintf( 'Dynamic tag "%s" not found.', $input['name'] ) ]; }
513 $tag = isset( $info['instance'] ) ? $info['instance'] : null;
514 return [ 'success' => true, 'tag' => [
515 'name' => (string) $input['name'],
516 'title' => is_object( $tag ) && method_exists( $tag, 'get_title' ) ? $tag->get_title() : '',
517 'group' => is_object( $tag ) && method_exists( $tag, 'get_group' ) ? $tag->get_group() : '',
518 ], 'message' => 'OK.' ];
519 } catch ( \Throwable $e ) {
520 return [ 'success' => false, 'message' => 'Failed to get dynamic tag: ' . $e->getMessage() ];
521 }
522 },
523 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
524 'meta' => $this->ro_meta(),
525 ] );
526
527 wp_register_ability( 'atarim/elementor-apply-dynamic-tag', [
528 'label' => 'Apply Elementor Dynamic Tag', 'category' => 'atarim',
529 'description' => 'EXPERIMENTAL. Bind a dynamic tag to one setting of an element (sets the element\'s __dynamic__ entry for that setting). Requires Elementor Pro. Read the element first with elementor-get-content.',
530 'input_schema' => [ 'type' => 'object', 'properties' => [
531 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'element_id' => [ 'type' => 'string' ], 'setting' => [ 'type' => 'string', 'description' => 'The control/setting name to bind.' ], 'tag_name' => [ 'type' => 'string' ], 'tag_settings' => [ 'type' => 'object' ],
532 ], 'required' => [ 'post_id', 'element_id', 'setting', 'tag_name' ], 'additionalProperties' => false ],
533 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
534 'execute_callback' => function( $input = [] ) use ( $self, $guard ) {
535 $g = $guard(); if ( $g ) { return $g; }
536 $post_id = (int) $input['post_id'];
537 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
538 if ( ! $self->post_can( $post_id ) ) { return [ 'success' => false, 'message' => 'No permission to edit this post.' ]; }
539 $eid = (string) $input['element_id']; $setting = (string) $input['setting']; $tag = (string) $input['tag_name'];
540 $tag_settings = isset( $input['tag_settings'] ) && is_array( $input['tag_settings'] ) ? $input['tag_settings'] : [];
541 $tag_id = substr( md5( uniqid( (string) wp_rand(), true ) ), 0, 7 );
542 $token = sprintf( '[elementor-tag id="%s" name="%s" settings="%s"]', $tag_id, $tag, rawurlencode( wp_json_encode( $tag_settings ) ) );
543 $tree = AVCF_Elementor_Helpers::read_tree( $post_id ); $found = false;
544 $tree = AVCF_Elementor_Helpers::map_edit( $tree, $eid, function( $node ) use ( $setting, $token ) {
545 $settings = isset( $node['settings'] ) && is_array( $node['settings'] ) ? $node['settings'] : [];
546 $dynamic = isset( $settings['__dynamic__'] ) && is_array( $settings['__dynamic__'] ) ? $settings['__dynamic__'] : [];
547 $dynamic[ $setting ] = $token;
548 $settings['__dynamic__'] = $dynamic;
549 $node['settings'] = $settings;
550 return $node;
551 }, $found );
552 if ( ! $found ) { return [ 'success' => false, 'message' => sprintf( 'Element "%s" not found.', $eid ) ]; }
553 if ( ! AVCF_Elementor_Helpers::write_tree( $post_id, $tree ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
554 return [ 'success' => true, 'message' => sprintf( 'Bound dynamic tag "%s" to setting "%s".', $tag, $setting ) ];
555 },
556 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
557 'meta' => $this->write_meta( false ),
558 ] );
559 }
560
561 /* ------------------------ interactions (3) ------------------------- */
562
563 private function register_interactions() {
564 $self = $this;
565
566 wp_register_ability( 'atarim/elementor-list-interactions', [
567 'label' => 'List Elementor Interactions', 'category' => 'atarim',
568 'description' => 'EXPERIMENTAL. List interactions configured on elements of a post (collected from each element\'s "interactions" setting).',
569 'input_schema' => [ 'type' => 'object', 'properties' => [ 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'post_id' ], 'additionalProperties' => false ],
570 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'interactions' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
571 'execute_callback' => function( $input = [] ) use ( $self ) {
572 $post_id = (int) $input['post_id'];
573 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
574 $tree = AVCF_Elementor_Helpers::read_tree( $post_id );
575 $out = [];
576 $walk = function( $els ) use ( &$walk, &$out ) {
577 foreach ( (array) $els as $el ) {
578 if ( ! is_array( $el ) ) { continue; }
579 if ( isset( $el['settings']['interactions'] ) && ! empty( $el['settings']['interactions'] ) ) {
580 $out[] = [ 'element_id' => isset( $el['id'] ) ? $el['id'] : '', 'interactions' => $el['settings']['interactions'] ];
581 }
582 if ( ! empty( $el['elements'] ) ) { $walk( $el['elements'] ); }
583 }
584 };
585 $walk( $tree );
586 return [ 'success' => true, 'interactions' => $out, 'message' => 'OK.' ];
587 },
588 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
589 'meta' => $this->ro_meta(),
590 ] );
591
592 wp_register_ability( 'atarim/elementor-add-interaction', [
593 'label' => 'Add Elementor Interaction', 'category' => 'atarim',
594 'description' => 'EXPERIMENTAL. Add an interaction definition to an element (appends to the element\'s "interactions" setting array). interaction is the Elementor interaction config object.',
595 'input_schema' => [ 'type' => 'object', 'properties' => [ 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'element_id' => [ 'type' => 'string' ], 'interaction' => [ 'type' => 'object' ] ], 'required' => [ 'post_id', 'element_id', 'interaction' ], 'additionalProperties' => false ],
596 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'interaction_id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
597 'execute_callback' => function( $input = [] ) use ( $self ) {
598 $post_id = (int) $input['post_id'];
599 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
600 if ( ! $self->post_can( $post_id ) ) { return [ 'success' => false, 'message' => 'No permission to edit this post.' ]; }
601 $eid = (string) $input['element_id'];
602 $interaction = is_array( $input['interaction'] ) ? $input['interaction'] : [];
603 $iid = 'i-' . $self->gen_id();
604 $interaction['id'] = $iid;
605 $tree = AVCF_Elementor_Helpers::read_tree( $post_id ); $found = false;
606 $tree = AVCF_Elementor_Helpers::map_edit( $tree, $eid, function( $node ) use ( $interaction ) {
607 $settings = isset( $node['settings'] ) && is_array( $node['settings'] ) ? $node['settings'] : [];
608 $list = isset( $settings['interactions'] ) && is_array( $settings['interactions'] ) ? $settings['interactions'] : [];
609 $list[] = $interaction;
610 $settings['interactions'] = $list;
611 $node['settings'] = $settings;
612 return $node;
613 }, $found );
614 if ( ! $found ) { return [ 'success' => false, 'message' => sprintf( 'Element "%s" not found.', $eid ) ]; }
615 if ( ! AVCF_Elementor_Helpers::write_tree( $post_id, $tree ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
616 return [ 'success' => true, 'interaction_id' => $iid, 'message' => 'Interaction added.' ];
617 },
618 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
619 'meta' => $this->write_meta( false ),
620 ] );
621
622 wp_register_ability( 'atarim/elementor-delete-interaction', [
623 'label' => 'Delete Elementor Interaction', 'category' => 'atarim',
624 'description' => 'EXPERIMENTAL. Remove an interaction from an element by its interaction id.',
625 'input_schema' => [ 'type' => 'object', 'properties' => [ 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'element_id' => [ 'type' => 'string' ], 'interaction_id' => [ 'type' => 'string' ] ], 'required' => [ 'post_id', 'element_id', 'interaction_id' ], 'additionalProperties' => false ],
626 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
627 'execute_callback' => function( $input = [] ) use ( $self ) {
628 $post_id = (int) $input['post_id'];
629 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
630 if ( ! $self->post_can( $post_id ) ) { return [ 'success' => false, 'message' => 'No permission to edit this post.' ]; }
631 $eid = (string) $input['element_id']; $iid = (string) $input['interaction_id'];
632 $tree = AVCF_Elementor_Helpers::read_tree( $post_id ); $found = false;
633 $tree = AVCF_Elementor_Helpers::map_edit( $tree, $eid, function( $node ) use ( $iid ) {
634 $settings = isset( $node['settings'] ) && is_array( $node['settings'] ) ? $node['settings'] : [];
635 if ( isset( $settings['interactions'] ) && is_array( $settings['interactions'] ) ) {
636 $settings['interactions'] = array_values( array_filter( $settings['interactions'], function( $i ) use ( $iid ) {
637 return ! ( is_array( $i ) && isset( $i['id'] ) && $i['id'] === $iid );
638 } ) );
639 }
640 $node['settings'] = $settings;
641 return $node;
642 }, $found );
643 if ( ! $found ) { return [ 'success' => false, 'message' => sprintf( 'Element "%s" not found.', $eid ) ]; }
644 if ( ! AVCF_Elementor_Helpers::write_tree( $post_id, $tree ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
645 return [ 'success' => true, 'message' => 'Interaction removed (if it existed).' ];
646 },
647 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
648 'meta' => $this->write_meta( true ),
649 ] );
650 }
651
652 /* ---------------------- delete-element-style (1) ------------------- */
653
654 private function register_element_style() {
655 $self = $this;
656 wp_register_ability( 'atarim/elementor-delete-element-style', [
657 'label' => 'Delete Elementor Element Style', 'category' => 'atarim',
658 'description' => 'EXPERIMENTAL. Clear an element\'s local style overrides — removes its atomic "styles" entries and resets style-tab settings — so it falls back to defaults / global styles. Dry run unless confirm:true.',
659 'input_schema' => [ 'type' => 'object', 'properties' => [ 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'element_id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'post_id', 'element_id' ], 'additionalProperties' => false ],
660 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
661 'execute_callback' => function( $input = [] ) use ( $self ) {
662 $post_id = (int) $input['post_id'];
663 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
664 if ( ! $self->post_can( $post_id ) ) { return [ 'success' => false, 'message' => 'No permission to edit this post.' ]; }
665 $eid = (string) $input['element_id'];
666 $tree = AVCF_Elementor_Helpers::read_tree( $post_id );
667 if ( AVCF_Elementor_Helpers::find( $tree, $eid ) === null ) { return [ 'success' => false, 'message' => sprintf( 'Element "%s" not found.', $eid ) ]; }
668 if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'cleared' => false, 'message' => sprintf( 'Dry run: would clear local styles on "%s". Re-call with confirm:true.', $eid ) ]; }
669 $found = false;
670 $tree = AVCF_Elementor_Helpers::map_edit( $tree, $eid, function( $node ) {
671 if ( isset( $node['styles'] ) ) { $node['styles'] = []; }
672 return $node;
673 }, $found );
674 if ( ! AVCF_Elementor_Helpers::write_tree( $post_id, $tree ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; }
675 return [ 'success' => true, 'cleared' => true, 'message' => sprintf( 'Cleared local styles on "%s".', $eid ) ];
676 },
677 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
678 'meta' => $this->write_meta( true ),
679 ] );
680 }
681
682 /* ---------------------- create-atomic-widget (1) ------------------- */
683
684 private function register_atomic_widget() {
685 $self = $this;
686 wp_register_ability( 'atarim/elementor-create-atomic-widget', [
687 'label' => 'Create Elementor Atomic Widget', 'category' => 'atarim',
688 'description' => 'EXPERIMENTAL. Insert an Elementor v4 atomic widget (e.g. "e-heading", "e-paragraph", "e-button") into a post. Atomic widgets are written via a RAW save because Elementor\'s Document::save() strips them. settings uses the atomic prop shape ({"$$type":...,"value":...}); pass them as you would see them in elementor-get-content. Validate output on a live Elementor build.',
689 'input_schema' => [ 'type' => 'object', 'properties' => [
690 'post_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'widget_type' => [ 'type' => 'string', 'description' => 'Atomic widget type, e.g. "e-heading".' ], 'parent_id' => [ 'type' => 'string' ], 'index' => [ 'type' => 'integer', 'minimum' => 0 ], 'settings' => [ 'type' => 'object' ], 'styles' => [ 'type' => 'object' ],
691 ], 'required' => [ 'post_id', 'widget_type' ], 'additionalProperties' => false ],
692 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'element_id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
693 'execute_callback' => function( $input = [] ) use ( $self ) {
694 $post_id = (int) $input['post_id'];
695 if ( $post_id <= 0 || ! get_post( $post_id ) ) { return [ 'success' => false, 'message' => 'A valid post_id is required.' ]; }
696 if ( ! $self->post_can( $post_id ) ) { return [ 'success' => false, 'message' => 'No permission to edit this post.' ]; }
697 $node = [
698 'id' => $self->gen_id(),
699 'elType' => 'widget',
700 'widgetType' => (string) $input['widget_type'],
701 'settings' => isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : (object) [],
702 'styles' => isset( $input['styles'] ) && is_array( $input['styles'] ) ? $input['styles'] : (object) [],
703 'elements' => [],
704 'editor_settings' => [],
705 'version' => '0.0',
706 ];
707 $tree = AVCF_Elementor_Helpers::read_tree( $post_id );
708 $index = isset( $input['index'] ) ? (int) $input['index'] : null;
709 list( $tree, $inserted ) = AVCF_Elementor_Helpers::insert( $tree, isset( $input['parent_id'] ) ? (string) $input['parent_id'] : null, $node, $index );
710 if ( ! $inserted ) { return [ 'success' => false, 'message' => sprintf( 'parent_id "%s" not found.', isset( $input['parent_id'] ) ? $input['parent_id'] : '' ) ]; }
711 // Force a raw write — Document::save() strips atomic widgets.
712 if ( ! AVCF_Elementor_Helpers::write_tree( $post_id, $tree, true ) ) { return [ 'success' => false, 'message' => 'Failed to save (raw).' ]; }
713 return [ 'success' => true, 'element_id' => $node['id'], 'message' => sprintf( 'Inserted atomic widget "%s" (raw write).', $node['widgetType'] ) ];
714 },
715 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
716 'meta' => $this->write_meta( false ),
717 ] );
718 }
719
720 /* ----------------------- validate-widget (1) ----------------------- */
721
722 private function register_validate_widget() {
723 $self = $this;
724 wp_register_ability( 'atarim/elementor-validate-widget', [
725 'label' => 'Validate Elementor Widget Settings', 'category' => 'atarim',
726 'description' => 'Check a settings object against a widget\'s controls: reports unknown setting keys (not in the widget\'s control list) and the list of valid control names. Use before add-element / edit-element to catch typos.',
727 'input_schema' => [ 'type' => 'object', 'properties' => [ 'widget' => [ 'type' => 'string' ], 'settings' => [ 'type' => 'object' ] ], 'required' => [ 'widget', 'settings' ], 'additionalProperties' => false ],
728 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'valid' => [ 'type' => 'boolean' ], 'unknown_keys' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ],
729 'execute_callback' => function( $input = [] ) {
730 $p = \Elementor\Plugin::$instance;
731 $wm = isset( $p->widgets_manager ) ? $p->widgets_manager : null;
732 if ( ! is_object( $wm ) ) { return [ 'success' => false, 'message' => 'Widgets manager unavailable.' ]; }
733 $name = (string) $input['widget'];
734 $settings = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : [];
735 try {
736 $widget = $wm->get_widget_types( $name );
737 if ( ! $widget ) { return [ 'success' => false, 'message' => sprintf( 'Widget "%s" not found.', $name ) ]; }
738 $controls = method_exists( $widget, 'get_controls' ) ? (array) $widget->get_controls() : [];
739 $valid_keys = array_keys( $controls );
740 $unknown = [];
741 foreach ( array_keys( $settings ) as $k ) {
742 if ( $k !== '__dynamic__' && ! in_array( $k, $valid_keys, true ) ) { $unknown[] = $k; }
743 }
744 return [ 'success' => true, 'valid' => empty( $unknown ), 'unknown_keys' => $unknown, 'message' => empty( $unknown ) ? 'All settings keys are valid controls.' : sprintf( '%d unknown key(s).', count( $unknown ) ) ];
745 } catch ( \Throwable $e ) {
746 return [ 'success' => false, 'message' => 'Validation failed: ' . $e->getMessage() ];
747 }
748 },
749 'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
750 'meta' => $this->ro_meta(),
751 ] );
752 }
753 }
754