atarim-visual-collaboration
/
third-party
/
page-builder
/
elementor
/
class-avcf-abilities-elementor-pro.php
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 |