fix: update ElementorTools to use v1.9.0 by-ref methods, normalize_container_settings, insertAfter for duplicate, correct addContainer defaults, wp_slash save path

This commit is contained in:
2026-06-02 20:37:30 +00:00
parent 157d296413
commit 96583831bb
+130 -148
View File
@@ -6,11 +6,11 @@ defined('ABSPATH') || exit;
* All Elementor MCP tools — pages, layouts, widgets, templates, globals. * All Elementor MCP tools — pages, layouts, widgets, templates, globals.
* No Freemius, no pro gates. All tools always available when Elementor is active. * No Freemius, no pro gates. All tools always available when Elementor is active.
* *
* Tool counts: * Updated to match methods in Elementor_MCP_Data v1.9.0:
* - Original 32 layout/widget/template/kit tools * - remove_element / insert_element / update_element_settings are by-reference (return bool)
* - +10 CSS/replace/custom-CSS/experiments/page-template tools * - updateElement / removeElement / moveElement / duplicateElement updated accordingly
* - +10 revisions/performance/CPT/maintenance/site-settings tools * - normalize_container_settings called on container updates (via data layer)
* Total: 52 Elementor tools / 200 plugin-wide * - save_page_data fallback uses wp_slash() + CSS cache invalidation
*/ */
class ElementorTools { class ElementorTools {
@@ -22,18 +22,17 @@ class ElementorTools {
public function getToolDefinitions(): array { public function getToolDefinitions(): array {
return [ return [
// ── Layout / Widget / Template / Kit (original 32) ─────────────
$this->def('elementor_create_page','Create a new WordPress page with Elementor enabled. Returns post_id, edit_url, preview_url.',['title'=>['type'=>'string'],'status'=>['type'=>'string','default'=>'draft','enum'=>['draft','publish']],'post_type'=>['type'=>'string','default'=>'page'],'template'=>['type'=>'string'],'content'=>['type'=>'array','items'=>['type'=>'object']]],['title'],'write','createPage'), $this->def('elementor_create_page','Create a new WordPress page with Elementor enabled. Returns post_id, edit_url, preview_url.',['title'=>['type'=>'string'],'status'=>['type'=>'string','default'=>'draft','enum'=>['draft','publish']],'post_type'=>['type'=>'string','default'=>'page'],'template'=>['type'=>'string'],'content'=>['type'=>'array','items'=>['type'=>'object']]],['title'],'write','createPage'),
$this->def('elementor_get_page','Get the full Elementor element tree JSON for a page.',['post_id'=>['type'=>'integer']],['post_id'],'read','getPage'), $this->def('elementor_get_page','Get the full Elementor element tree JSON for a page.',['post_id'=>['type'=>'integer']],['post_id'],'read','getPage'),
$this->def('elementor_update_page_settings','Update page-level Elementor settings (background, padding, custom CSS, layout).',['post_id'=>['type'=>'integer'],'settings'=>['type'=>'object']],['post_id','settings'],'write','updatePageSettings'), $this->def('elementor_update_page_settings','Update page-level Elementor settings (background, padding, custom CSS, layout).',['post_id'=>['type'=>'integer'],'settings'=>['type'=>'object']],['post_id','settings'],'write','updatePageSettings'),
$this->def('elementor_delete_page_content','Clear all Elementor content from a page, resetting it to blank.',['post_id'=>['type'=>'integer']],['post_id'],'write','deletePageContent'), $this->def('elementor_delete_page_content','Clear all Elementor content from a page, resetting it to blank.',['post_id'=>['type'=>'integer']],['post_id'],'write','deletePageContent'),
$this->def('elementor_import_template','Import a JSON element structure into a page at an optional position.',['post_id'=>['type'=>'integer'],'template_json'=>['type'=>'array','items'=>['type'=>'object']],'position'=>['type'=>'integer']],['post_id','template_json'],'write','importTemplate'), $this->def('elementor_import_template','Import a JSON element structure into a page at an optional position.',['post_id'=>['type'=>'integer'],'template_json'=>['type'=>'array','items'=>['type'=>'object']],'position'=>['type'=>'integer']],['post_id','template_json'],'write','importTemplate'),
$this->def('elementor_export_page','Export a page\'s full Elementor data as JSON.',['post_id'=>['type'=>'integer']],['post_id'],'read','exportPage'), $this->def('elementor_export_page','Export a page\'s full Elementor data as JSON.',['post_id'=>['type'=>'integer']],['post_id'],'read','exportPage'),
$this->def('elementor_add_container','Add a flex or grid container to an Elementor page. Use parent_id for nesting, position for insertion index (-1 = append).',['post_id'=>['type'=>'integer'],'parent_id'=>['type'=>'string'],'position'=>['type'=>'integer'],'settings'=>['type'=>'object']],['post_id'],'write','addContainer'), $this->def('elementor_add_container','Add a flex or grid container to an Elementor page. Use parent_id for nesting, position for insertion index (-1 = append). Settings: flex_direction, flex_wrap, flex_justify_content (or justify_content), flex_align_items (or align_items), gap, padding, background_color, border_border, etc. Shorthand justify_content/align_items/align_content are auto-remapped to flex_justify_content/flex_align_items/flex_align_content.',['post_id'=>['type'=>'integer'],'parent_id'=>['type'=>'string'],'position'=>['type'=>'integer'],'settings'=>['type'=>'object']],['post_id'],'write','addContainer'),
$this->def('elementor_update_container','Update settings on an existing container element.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'settings'=>['type'=>'object']],['post_id','element_id'],'write','updateContainer'), $this->def('elementor_update_container','Update settings on an existing container element. Accepts flex shorthand justify_content/align_items/align_content and remaps to prefixed keys automatically.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'settings'=>['type'=>'object']],['post_id','element_id'],'write','updateContainer'),
$this->def('elementor_update_element','Update settings on any Elementor element by ID.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'settings'=>['type'=>'object'],'merge'=>['type'=>'boolean','default'=>true]],['post_id','element_id','settings'],'write','updateElement'), $this->def('elementor_update_element','Update settings on any Elementor element by ID. For containers, flex shorthand keys are auto-normalised.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'settings'=>['type'=>'object'],'merge'=>['type'=>'boolean','default'=>true]],['post_id','element_id','settings'],'write','updateElement'),
$this->def('elementor_remove_element','Remove any element (container or widget) from a page by element ID.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string']],['post_id','element_id'],'write','removeElement'), $this->def('elementor_remove_element','Remove any element (container or widget) from a page by element ID.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string']],['post_id','element_id'],'write','removeElement'),
$this->def('elementor_duplicate_element','Duplicate an element (and its children) within the same page.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'position'=>['type'=>'integer']],['post_id','element_id'],'write','duplicateElement'), $this->def('elementor_duplicate_element','Duplicate an element (and its children) within the same page. Clone is placed immediately after the original.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'position'=>['type'=>'integer']],['post_id','element_id'],'write','duplicateElement'),
$this->def('elementor_move_element','Move an element to a different parent or position within the page.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'target_parent_id'=>['type'=>'string'],'position'=>['type'=>'integer']],['post_id','element_id'],'write','moveElement'), $this->def('elementor_move_element','Move an element to a different parent or position within the page.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'target_parent_id'=>['type'=>'string'],'position'=>['type'=>'integer']],['post_id','element_id'],'write','moveElement'),
$this->def('elementor_add_widget','Add any Elementor widget to a container. widget_type: heading, text-editor, image, button, video, icon, divider, spacer, google_maps, html, shortcode, accordion, tabs, toggle, icon-list, counter, progress, testimonial, alert, social-icons, star-rating, image-box, icon-box, basic-gallery, image-carousel, reviews, posts, nav-menu, search-form, woocommerce-product-images, etc.',['post_id'=>['type'=>'integer'],'container_id'=>['type'=>'string'],'widget_type'=>['type'=>'string'],'settings'=>['type'=>'object'],'position'=>['type'=>'integer']],['post_id','container_id','widget_type'],'write','addWidget'), $this->def('elementor_add_widget','Add any Elementor widget to a container. widget_type: heading, text-editor, image, button, video, icon, divider, spacer, google_maps, html, shortcode, accordion, tabs, toggle, icon-list, counter, progress, testimonial, alert, social-icons, star-rating, image-box, icon-box, basic-gallery, image-carousel, reviews, posts, nav-menu, search-form, woocommerce-product-images, etc.',['post_id'=>['type'=>'integer'],'container_id'=>['type'=>'string'],'widget_type'=>['type'=>'string'],'settings'=>['type'=>'object'],'position'=>['type'=>'integer']],['post_id','container_id','widget_type'],'write','addWidget'),
$this->def('elementor_update_widget','Update an existing widget\'s settings.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'settings'=>['type'=>'object'],'merge'=>['type'=>'boolean','default'=>true]],['post_id','element_id','settings'],'write','updateWidget'), $this->def('elementor_update_widget','Update an existing widget\'s settings.',['post_id'=>['type'=>'integer'],'element_id'=>['type'=>'string'],'settings'=>['type'=>'object'],'merge'=>['type'=>'boolean','default'=>true]],['post_id','element_id','settings'],'write','updateWidget'),
@@ -55,11 +54,9 @@ class ElementorTools {
$this->def('elementor_replace_text','Replace all occurrences of a text string across all widgets in a page.',['post_id'=>['type'=>'integer'],'search'=>['type'=>'string'],'replace'=>['type'=>'string'],'case_sensitive'=>['type'=>'boolean','default'=>false]],['post_id','search','replace'],'write','replaceText'), $this->def('elementor_replace_text','Replace all occurrences of a text string across all widgets in a page.',['post_id'=>['type'=>'integer'],'search'=>['type'=>'string'],'replace'=>['type'=>'string'],'case_sensitive'=>['type'=>'boolean','default'=>false]],['post_id','search','replace'],'write','replaceText'),
$this->def('elementor_get_page_outline','Get a simplified structural outline of an Elementor page (containers + widget types).',['post_id'=>['type'=>'integer']],['post_id'],'read','getPageOutline'), $this->def('elementor_get_page_outline','Get a simplified structural outline of an Elementor page (containers + widget types).',['post_id'=>['type'=>'integer']],['post_id'],'read','getPageOutline'),
$this->def('elementor_delete_items','Delete one or more elements (containers or widgets) from a page by element_id. Use element_ids array.',['post_id'=>['type'=>'integer'],'element_ids'=>['type'=>'array','items'=>['type'=>'string']],'element_id'=>['type'=>'string']],['post_id'],'write','deleteItems'), $this->def('elementor_delete_items','Delete one or more elements (containers or widgets) from a page by element_id. Use element_ids array.',['post_id'=>['type'=>'integer'],'element_ids'=>['type'=>'array','items'=>['type'=>'string']],'element_id'=>['type'=>'string']],['post_id'],'write','deleteItems'),
// ── CSS / Regeneration / Migration (+10) ───────────────────────
$this->def('elementor_flush_css','Flush and regenerate all Elementor CSS files across the entire site. Call after bulk design changes.',[],[],'write','flushCss'), $this->def('elementor_flush_css','Flush and regenerate all Elementor CSS files across the entire site. Call after bulk design changes.',[],[],'write','flushCss'),
$this->def('elementor_regenerate_css','Regenerate the Elementor CSS file for a single post/page.',['post_id'=>['type'=>'integer']],['post_id'],'write','regenerateCss'), $this->def('elementor_regenerate_css','Regenerate the Elementor CSS file for a single post/page.',['post_id'=>['type'=>'integer']],['post_id'],'write','regenerateCss'),
$this->def('elementor_replace_urls','Find and replace a URL string across ALL Elementor data in the database. Essential for domain migrations.',['old_url'=>['type'=>'string'],'new_url'=>['type'=>'string']],['old_url','new_url'],'admin','replaceUrls'), $this->def('elementor_replace_urls','Find and replace a URL string across ALL Elementor data in the database. Essential for domain migrations and staging to production pushes.',['old_url'=>['type'=>'string'],'new_url'=>['type'=>'string']],['old_url','new_url'],'admin','replaceUrls'),
$this->def('elementor_get_kit_custom_css','Get the global custom CSS stored in the Elementor Site Kit.',[],[],'read','getKitCustomCss'), $this->def('elementor_get_kit_custom_css','Get the global custom CSS stored in the Elementor Site Kit.',[],[],'read','getKitCustomCss'),
$this->def('elementor_update_kit_custom_css','Set the global custom CSS in the Elementor Site Kit. Replaces existing kit-level CSS.',['css'=>['type'=>'string']],['css'],'write','updateKitCustomCss'), $this->def('elementor_update_kit_custom_css','Set the global custom CSS in the Elementor Site Kit. Replaces existing kit-level CSS.',['css'=>['type'=>'string']],['css'],'write','updateKitCustomCss'),
$this->def('elementor_get_page_custom_css','Get the custom CSS set for a specific Elementor page (page-level, not kit-level).',['post_id'=>['type'=>'integer']],['post_id'],'read','getPageCustomCss'), $this->def('elementor_get_page_custom_css','Get the custom CSS set for a specific Elementor page (page-level, not kit-level).',['post_id'=>['type'=>'integer']],['post_id'],'read','getPageCustomCss'),
@@ -67,18 +64,16 @@ class ElementorTools {
$this->def('elementor_list_experiments','List all Elementor feature flags (experiments) with their current state.',[],[],'read','listExperiments'), $this->def('elementor_list_experiments','List all Elementor feature flags (experiments) with their current state.',[],[],'read','listExperiments'),
$this->def('elementor_set_experiment','Activate or deactivate an Elementor feature flag by name.',['name'=>['type'=>'string'],'state'=>['type'=>'string','enum'=>['active','inactive','default']]],['name','state'],'admin','setExperiment'), $this->def('elementor_set_experiment','Activate or deactivate an Elementor feature flag by name.',['name'=>['type'=>'string'],'state'=>['type'=>'string','enum'=>['active','inactive','default']]],['name','state'],'admin','setExperiment'),
$this->def('elementor_set_page_template','Set the WordPress page template for a post (e.g. elementor_canvas, elementor-full-width, default).',['post_id'=>['type'=>'integer'],'template'=>['type'=>'string']],['post_id','template'],'write','setPageTemplate'), $this->def('elementor_set_page_template','Set the WordPress page template for a post (e.g. elementor_canvas, elementor-full-width, default).',['post_id'=>['type'=>'integer'],'template'=>['type'=>'string']],['post_id','template'],'write','setPageTemplate'),
// ── Revisions / Performance / CPT / Maintenance / Site (+10) ───
$this->def('elementor_get_revisions','List all saved revisions for an Elementor post. Returns id, type, author, timestamp.',['post_id'=>['type'=>'integer']],['post_id'],'read','getRevisions'), $this->def('elementor_get_revisions','List all saved revisions for an Elementor post. Returns id, type, author, timestamp.',['post_id'=>['type'=>'integer']],['post_id'],'read','getRevisions'),
$this->def('elementor_restore_revision','Restore an Elementor post to a specific revision by revision ID.',['post_id'=>['type'=>'integer'],'revision_id'=>['type'=>'integer']],['post_id','revision_id'],'write','restoreRevision'), $this->def('elementor_restore_revision','Restore an Elementor post to a specific revision by revision ID.',['post_id'=>['type'=>'integer'],'revision_id'=>['type'=>'integer']],['post_id','revision_id'],'write','restoreRevision'),
$this->def('elementor_get_performance_settings','Get all Elementor performance settings (CSS print method, font display, lazy load, FA4 shim, caching, etc.).',[],[],'read','getPerformanceSettings'), $this->def('elementor_get_performance_settings','Get all Elementor performance settings (CSS print method, font display, lazy load, FA4 shim, caching, etc.).',[],[],'read','getPerformanceSettings'),
$this->def('elementor_update_performance_settings','Update one or more Elementor performance settings. Fields: css_print_method (external|internal), google_font (1|), load_fa4_shim (1|), lazy_load_background_images (1|), meta_generator_tag (1|), unfiltered_files_upload (1|), font_display (auto|block|swap|fallback|optional), google_maps_api_key (string), element_cache_ttl (int hours), optimized_gutenberg_loading (1|).',['settings'=>['type'=>'object']],['settings'],'admin','updatePerformanceSettings'), $this->def('elementor_update_performance_settings','Update one or more Elementor performance settings.',['settings'=>['type'=>'object']],['settings'],'admin','updatePerformanceSettings'),
$this->def('elementor_get_cpt_support','Get the list of post types where Elementor editor is enabled.',[],[],'read','getCptSupport'), $this->def('elementor_get_cpt_support','Get the list of post types where Elementor editor is enabled.',[],[],'read','getCptSupport'),
$this->def('elementor_set_cpt_support','Set which post types Elementor editor is enabled for. Pass post_types as array of slugs.',['post_types'=>['type'=>'array','items'=>['type'=>'string']]],['post_types'],'admin','setCptSupport'), $this->def('elementor_set_cpt_support','Set which post types Elementor editor is enabled for. Pass post_types as array of slugs.',['post_types'=>['type'=>'array','items'=>['type'=>'string']]],['post_types'],'admin','setCptSupport'),
$this->def('elementor_get_maintenance_mode','Get current Elementor maintenance/coming-soon mode state.',[],[],'read','getMaintenanceMode'), $this->def('elementor_get_maintenance_mode','Get current Elementor maintenance/coming-soon mode state.',[],[],'read','getMaintenanceMode'),
$this->def('elementor_set_maintenance_mode','Enable or disable Elementor maintenance or coming-soon mode.',['mode'=>['type'=>'string','enum'=>['maintenance','coming_soon','']],'page_id'=>['type'=>'integer'],'exclude_mode'=>['type'=>'string','enum'=>['logged_in','exclude_admins','']]],['mode'],'admin','setMaintenanceMode'), $this->def('elementor_set_maintenance_mode','Enable or disable Elementor maintenance or coming-soon mode.',['mode'=>['type'=>'string','enum'=>['maintenance','coming_soon','']],'page_id'=>['type'=>'integer'],'exclude_mode'=>['type'=>'string','enum'=>['logged_in','exclude_admins','']]],['mode'],'admin','setMaintenanceMode'),
$this->def('elementor_get_site_settings','Get Elementor site-level settings: Google Maps API key, font settings, CSS method, and other global options.',[],[],'read','getSiteSettings'), $this->def('elementor_get_site_settings','Get Elementor site-level settings: Google Maps API key, meta generator tag, font settings, and other global options.',[],[],'read','getSiteSettings'),
$this->def('elementor_update_site_settings','Update Elementor site-level settings. Fields: google_maps_api_key, meta_generator_tag, google_font, font_display, load_fa4_shim, unfiltered_files_upload, css_print_method.',['settings'=>['type'=>'object']],['settings'],'admin','updateSiteSettings'), $this->def('elementor_update_site_settings','Update Elementor site-level settings.',['settings'=>['type'=>'object']],['settings'],'admin','updateSiteSettings'),
]; ];
} }
@@ -88,10 +83,6 @@ class ElementorTools {
return ['name'=>$name,'description'=>$desc,'scope'=>$scope,'handler'=>[$this,$method],'inputSchema'=>$schema]; return ['name'=>$name,'description'=>$desc,'scope'=>$scope,'handler'=>[$this,$method],'inputSchema'=>$schema];
} }
// =========================================================
// LAYOUT / WIDGET / TEMPLATE / KIT METHODS
// =========================================================
public function createPage(array $args): array { public function createPage(array $args): array {
$title = sanitize_text_field($args['title'] ?? ''); $title = sanitize_text_field($args['title'] ?? '');
$status = sanitize_key($args['status'] ?? 'draft'); $status = sanitize_key($args['status'] ?? 'draft');
@@ -149,24 +140,37 @@ class ElementorTools {
return ['json'=>$data,'count'=>$this->data->count_elements($data)]; return ['json'=>$data,'count'=>$this->data->count_elements($data)];
} }
/**
* Add a container. Uses insert_element() (by-reference) from the data layer.
* Flex shorthand keys normalised before building the element.
* Auto-sets flex_align_items=center for column containers (mirrors v1.9.0 factory defaults).
*/
public function addContainer(array $args): array { public function addContainer(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
$parent_id = $args['parent_id'] ?? null; $parent_id = $args['parent_id'] ?? '';
$position = (int)($args['position'] ?? -1); $position = (int)($args['position'] ?? -1);
$settings = $args['settings'] ?? []; $settings = $args['settings'] ?? [];
foreach (['justify_content'=>'flex_justify_content','align_items'=>'flex_align_items','align_content'=>'flex_align_content'] as $from=>$to) {
if (isset($settings[$from]) && !isset($settings[$to])) { $settings[$to] = $settings[$from]; unset($settings[$from]); } $settings = \AskTamMCP\Elementor\Data\ElementorData::normalize_container_settings($settings);
}
$data = $this->data->get_page_data($post_id); $data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message()); if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$element_id = $this->data->generate_id(); $element_id = $this->data->generate_id();
$container = ['id'=>$element_id,'elType'=>'container','settings'=>array_merge(['flex_direction'=>'row'],$settings),'elements'=>[],'isInner'=>false]; $defaults = ['container_type'=>'flex','content_width'=>'boxed'];
if ($parent_id) {
$data = $this->insertIntoParent($data, $parent_id, $container, $position); $direction = $settings['flex_direction'] ?? '';
} else { $is_row = in_array($direction, ['row','row-reverse'], true);
if ($position < 0 || $position >= count($data)) $data[] = $container; $is_grid = ($settings['container_type'] ?? 'flex') === 'grid';
else array_splice($data, $position, 0, [$container]); if (!$is_grid && !$is_row && !isset($settings['flex_align_items'])) {
$defaults['flex_align_items'] = 'center';
} }
$container = ['id'=>$element_id,'elType'=>'container','widgetType'=>null,'isInner'=>!empty($parent_id),'settings'=>array_merge($defaults,$settings),'elements'=>[]];
$inserted = $this->data->insert_element($data, $parent_id, $container, $position);
if (!$inserted) throw new \RuntimeException("Parent element '{$parent_id}' not found.");
$result = $this->data->save_page_data($post_id, $data); $result = $this->data->save_page_data($post_id, $data);
if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message()); if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message());
return ['element_id'=>$element_id,'post_id'=>$post_id]; return ['element_id'=>$element_id,'post_id'=>$post_id];
@@ -174,62 +178,88 @@ class ElementorTools {
public function updateContainer(array $args): array { return $this->updateElement($args); } public function updateContainer(array $args): array { return $this->updateElement($args); }
/**
* Update any element. For containers the data layer auto-normalises flex shorthand keys.
* update_element_settings() is by-reference and returns bool.
*/
public function updateElement(array $args): array { public function updateElement(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
$element_id = $args['element_id'] ?? ''; $element_id = $args['element_id'] ?? '';
$settings = $args['settings'] ?? []; $settings = $args['settings'] ?? [];
$merge = (bool)($args['merge'] ?? true); $merge = (bool)($args['merge'] ?? true);
$data = $this->data->get_page_data($post_id); $data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message()); if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$data = $this->data->update_element_settings($data, $element_id, $settings, $merge);
$updated = $this->data->update_element_settings($data, $element_id, $settings, $merge);
if (!$updated) throw new \RuntimeException("Element not found: {$element_id}");
$result = $this->data->save_page_data($post_id, $data); $result = $this->data->save_page_data($post_id, $data);
if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message()); if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message());
return ['success'=>true,'post_id'=>$post_id,'element_id'=>$element_id]; return ['success'=>true,'post_id'=>$post_id,'element_id'=>$element_id];
} }
/**
* Remove an element. remove_element() is by-reference and returns bool.
*/
public function removeElement(array $args): array { public function removeElement(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
$element_id = $args['element_id'] ?? ''; $element_id = $args['element_id'] ?? '';
$data = $this->data->get_page_data($post_id); $data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message()); if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$data = $this->data->remove_element($data, $element_id);
$removed = $this->data->remove_element($data, $element_id);
if (!$removed) throw new \RuntimeException("Element not found: {$element_id}");
$result = $this->data->save_page_data($post_id, $data); $result = $this->data->save_page_data($post_id, $data);
if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message()); if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message());
return ['success'=>true,'removed_id'=>$element_id]; return ['success'=>true,'removed_id'=>$element_id];
} }
/**
* Duplicate element — clone placed immediately after original (mirrors v1.9.0 insert_after).
*/
public function duplicateElement(array $args): array { public function duplicateElement(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
$element_id = $args['element_id'] ?? ''; $element_id = $args['element_id'] ?? '';
$position = (int)($args['position'] ?? -1);
$data = $this->data->get_page_data($post_id); $data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message()); if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$element = $this->data->find_element($data, $element_id); $element = $this->data->find_element($data, $element_id);
if (!$element) throw new \RuntimeException("Element not found: {$element_id}"); if (!$element) throw new \RuntimeException("Element not found: {$element_id}");
$clone = $this->data->reassign_ids([$element])[0];
if ($position < 0 || $position >= count($data)) $data[] = $clone; $clone = $this->data->reassign_element_ids($element);
else array_splice($data, $position, 0, [$clone]); $inserted = $this->insertAfter($data, $element_id, $clone);
if (!$inserted) throw new \RuntimeException("Failed to insert duplicate after {$element_id}.");
$result = $this->data->save_page_data($post_id, $data); $result = $this->data->save_page_data($post_id, $data);
if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message()); if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message());
return ['success'=>true,'new_element_id'=>$clone['id']]; return ['success'=>true,'new_element_id'=>$clone['id']];
} }
/**
* Move element — by-reference remove then insert.
*/
public function moveElement(array $args): array { public function moveElement(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
$element_id = $args['element_id'] ?? ''; $element_id = $args['element_id'] ?? '';
$target_parent_id = $args['target_parent_id'] ?? null; $target_parent_id = $args['target_parent_id'] ?? '';
$position = (int)($args['position'] ?? -1); $position = (int)($args['position'] ?? -1);
$data = $this->data->get_page_data($post_id); $data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message()); if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$element = $this->data->find_element($data, $element_id); $element = $this->data->find_element($data, $element_id);
if (!$element) throw new \RuntimeException("Element not found: {$element_id}"); if (!$element) throw new \RuntimeException("Element not found: {$element_id}");
$data = $this->data->remove_element($data, $element_id);
if ($target_parent_id) { $removed = $this->data->remove_element($data, $element_id);
$data = $this->insertIntoParent($data, $target_parent_id, $element, $position); if (!$removed) throw new \RuntimeException("Failed to remove element: {$element_id}");
} else {
if ($position < 0 || $position >= count($data)) $data[] = $element; $inserted = $this->data->insert_element($data, $target_parent_id, $element, $position);
else array_splice($data, $position, 0, [$element]); if (!$inserted) throw new \RuntimeException("Failed to insert element at target '{$target_parent_id}'.");
}
$result = $this->data->save_page_data($post_id, $data); $result = $this->data->save_page_data($post_id, $data);
if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message()); if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message());
return ['success'=>true,'element_id'=>$element_id]; return ['success'=>true,'element_id'=>$element_id];
@@ -242,11 +272,20 @@ class ElementorTools {
$settings = $args['settings'] ?? []; $settings = $args['settings'] ?? [];
$position = (int)($args['position'] ?? -1); $position = (int)($args['position'] ?? -1);
if (empty($widget_type)) throw new \RuntimeException('widget_type is required.'); if (empty($widget_type)) throw new \RuntimeException('widget_type is required.');
$data = $this->data->get_page_data($post_id); $data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message()); if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$element_id = $this->data->generate_id(); $element_id = $this->data->generate_id();
$widget = ['id'=>$element_id,'elType'=>'widget','widgetType'=>$widget_type,'settings'=>$settings,'elements'=>[]]; $widget = ['id'=>$element_id,'elType'=>'widget','widgetType'=>$widget_type,'isInner'=>false,'settings'=>$settings,'elements'=>[]];
$inserted = $this->data->insert_element($data, $container_id, $widget, $position);
if (!$inserted && !empty($container_id)) {
$data = $this->data->get_page_data($post_id);
if (is_wp_error($data)) throw new \RuntimeException($data->get_error_message());
$data = $this->insertIntoParent($data, $container_id, $widget, $position); $data = $this->insertIntoParent($data, $container_id, $widget, $position);
}
$result = $this->data->save_page_data($post_id, $data); $result = $this->data->save_page_data($post_id, $data);
if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message()); if (is_wp_error($result)) throw new \RuntimeException($result->get_error_message());
return ['element_id'=>$element_id,'post_id'=>$post_id,'widget_type'=>$widget_type]; return ['element_id'=>$element_id,'post_id'=>$post_id,'widget_type'=>$widget_type];
@@ -428,10 +467,6 @@ class ElementorTools {
return ['post_id'=>$args['post_id'],'deleted'=>$deleted,'count'=>count($deleted)]; return ['post_id'=>$args['post_id'],'deleted'=>$deleted,'count'=>count($deleted)];
} }
// =========================================================
// CSS / REGENERATION / MIGRATION METHODS
// =========================================================
public function flushCss(array $args): array { public function flushCss(array $args): array {
if (!class_exists('\Elementor\Plugin')) throw new \RuntimeException('Elementor not active.'); if (!class_exists('\Elementor\Plugin')) throw new \RuntimeException('Elementor not active.');
\Elementor\Plugin::instance()->files_manager->clear_cache(); \Elementor\Plugin::instance()->files_manager->clear_cache();
@@ -441,8 +476,8 @@ class ElementorTools {
public function regenerateCss(array $args): array { public function regenerateCss(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
if (!$post_id) throw new \RuntimeException('post_id is required.'); if (!$post_id) throw new \RuntimeException('post_id is required.');
$css_file = new \Elementor\Core\Files\CSS\Post($post_id); if (!class_exists('\Elementor\Core\Files\CSS\Post')) throw new \RuntimeException('Elementor CSS class not found.');
$css_file->update(); (new \Elementor\Core\Files\CSS\Post($post_id))->update();
return ['success'=>true,'post_id'=>$post_id,'message'=>"CSS regenerated for post {$post_id}."]; return ['success'=>true,'post_id'=>$post_id,'message'=>"CSS regenerated for post {$post_id}."];
} }
@@ -456,21 +491,18 @@ class ElementorTools {
"SELECT pm.post_id FROM {$wpdb->postmeta} pm WHERE pm.meta_key='_elementor_data' AND pm.meta_value LIKE %s", "SELECT pm.post_id FROM {$wpdb->postmeta} pm WHERE pm.meta_key='_elementor_data' AND pm.meta_value LIKE %s",
'%'.$wpdb->esc_like($old).'%' '%'.$wpdb->esc_like($old).'%'
)); ));
$updated = 0; $updated = 0; $errors = [];
$errors = [];
foreach ($posts as $pid) { foreach ($posts as $pid) {
$raw = get_post_meta((int)$pid, '_elementor_data', true); $raw = get_post_meta((int)$pid, '_elementor_data', true);
if (empty($raw)) continue; if (empty($raw)) continue;
$decoded = json_decode($raw, true); $decoded = json_decode($raw, true);
$new_raw = str_replace($old, $new, is_array($decoded) ? wp_json_encode($decoded) : $raw); $new_raw = str_replace($old, $new, is_array($decoded) ? wp_json_encode($decoded) : $raw);
if ($new_raw === $raw) continue; if ($new_raw === $raw) continue;
$result = update_post_meta((int)$pid, '_elementor_data', wp_slash($new_raw)); $r = update_post_meta((int)$pid, '_elementor_data', wp_slash($new_raw));
if ($result !== false) { if ($r !== false) {
if (class_exists('\Elementor\Core\Files\CSS\Post')) (new \Elementor\Core\Files\CSS\Post((int)$pid))->update(); if (class_exists('\Elementor\Core\Files\CSS\Post')) (new \Elementor\Core\Files\CSS\Post((int)$pid))->update();
$updated++; $updated++;
} else { } else { $errors[] = (int)$pid; }
$errors[] = (int)$pid;
}
} }
return ['success'=>true,'old_url'=>$old,'new_url'=>$new,'posts_found'=>count($posts),'updated'=>$updated,'errors'=>$errors]; return ['success'=>true,'old_url'=>$old,'new_url'=>$new,'posts_found'=>count($posts),'updated'=>$updated,'errors'=>$errors];
} }
@@ -529,11 +561,15 @@ class ElementorTools {
$feature = $experiments->get_features($name); $feature = $experiments->get_features($name);
if (!$feature) throw new \RuntimeException("Experiment '{$name}' not found."); if (!$feature) throw new \RuntimeException("Experiment '{$name}' not found.");
$option_key = $experiments->get_feature_option_key($name); $option_key = $experiments->get_feature_option_key($name);
if ($state === 'default') delete_option($option_key); if ($state === 'default') delete_option($option_key); else update_option($option_key, $state);
else update_option($option_key, $state);
return ['success'=>true,'name'=>$name,'state'=>$state,'is_active'=>$experiments->is_feature_active($name)]; return ['success'=>true,'name'=>$name,'state'=>$state,'is_active'=>$experiments->is_feature_active($name)];
} }
/**
* Set page template — always uses update_post_meta directly.
* wp_update_post template validation fails in CLI context (no registered templates).
* See 05-KNOWN-ISSUES.md.
*/
public function setPageTemplate(array $args): array { public function setPageTemplate(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
$template = $args['template'] ?? ''; $template = $args['template'] ?? '';
@@ -544,10 +580,6 @@ class ElementorTools {
return ['success'=>true,'post_id'=>$post_id,'template'=>$template?:'default']; return ['success'=>true,'post_id'=>$post_id,'template'=>$template?:'default'];
} }
// =========================================================
// REVISIONS
// =========================================================
public function getRevisions(array $args): array { public function getRevisions(array $args): array {
$post_id = (int)($args['post_id'] ?? 0); $post_id = (int)($args['post_id'] ?? 0);
if (!$post_id) throw new \RuntimeException('post_id is required.'); if (!$post_id) throw new \RuntimeException('post_id is required.');
@@ -567,47 +599,19 @@ class ElementorTools {
return ['success'=>true,'post_id'=>$post_id,'restored_revision_id'=>$revision_id]; return ['success'=>true,'post_id'=>$post_id,'restored_revision_id'=>$revision_id];
} }
// =========================================================
// PERFORMANCE SETTINGS
// =========================================================
public function getPerformanceSettings(array $args): array { public function getPerformanceSettings(array $args): array {
return [ return ['css_print_method'=>get_option('elementor_css_print_method','external'),'google_font'=>get_option('elementor_google_font','1'),'load_fa4_shim'=>get_option('elementor_load_fa4_shim',''),'lazy_load_background_images'=>get_option('elementor_lazy_load_background_images','1'),'meta_generator_tag'=>get_option('elementor_meta_generator_tag',''),'unfiltered_files_upload'=>get_option('elementor_unfiltered_files_upload',''),'font_display'=>get_option('elementor_font_display','auto'),'google_maps_api_key'=>get_option('elementor_google_maps_api_key',''),'element_cache_ttl'=>(int)get_option('elementor_element_cache_ttl',24),'optimized_gutenberg_loading'=>get_option('elementor_optimized_gutenberg_loading','1')];
'css_print_method' => get_option('elementor_css_print_method', 'external'),
'google_font' => get_option('elementor_google_font', '1'),
'load_fa4_shim' => get_option('elementor_load_fa4_shim', ''),
'lazy_load_background_images' => get_option('elementor_lazy_load_background_images', '1'),
'meta_generator_tag' => get_option('elementor_meta_generator_tag', ''),
'unfiltered_files_upload' => get_option('elementor_unfiltered_files_upload', ''),
'font_display' => get_option('elementor_font_display', 'auto'),
'google_maps_api_key' => get_option('elementor_google_maps_api_key', ''),
'element_cache_ttl' => (int)get_option('elementor_element_cache_ttl', 24),
'optimized_gutenberg_loading' => get_option('elementor_optimized_gutenberg_loading', '1'),
];
} }
public function updatePerformanceSettings(array $args): array { public function updatePerformanceSettings(array $args): array {
$settings = $args['settings'] ?? []; $settings = $args['settings'] ?? [];
if (empty($settings)) throw new \RuntimeException('settings object is required.'); if (empty($settings)) throw new \RuntimeException('settings object is required.');
$map = [ $map = ['css_print_method'=>['key'=>'elementor_css_print_method','allowed'=>['external','internal']],'google_font'=>['key'=>'elementor_google_font','allowed'=>['1','']],'load_fa4_shim'=>['key'=>'elementor_load_fa4_shim','allowed'=>['1','']],'lazy_load_background_images'=>['key'=>'elementor_lazy_load_background_images','allowed'=>['1','']],'meta_generator_tag'=>['key'=>'elementor_meta_generator_tag','allowed'=>['1','']],'unfiltered_files_upload'=>['key'=>'elementor_unfiltered_files_upload','allowed'=>['1','']],'font_display'=>['key'=>'elementor_font_display','allowed'=>['auto','block','swap','fallback','optional']],'google_maps_api_key'=>['key'=>'elementor_google_maps_api_key','allowed'=>null],'element_cache_ttl'=>['key'=>'elementor_element_cache_ttl','allowed'=>null,'type'=>'int'],'optimized_gutenberg_loading'=>['key'=>'elementor_optimized_gutenberg_loading','allowed'=>['1','']]];
'css_print_method' => ['key'=>'elementor_css_print_method', 'allowed'=>['external','internal']],
'google_font' => ['key'=>'elementor_google_font', 'allowed'=>['1','']],
'load_fa4_shim' => ['key'=>'elementor_load_fa4_shim', 'allowed'=>['1','']],
'lazy_load_background_images' => ['key'=>'elementor_lazy_load_background_images', 'allowed'=>['1','']],
'meta_generator_tag' => ['key'=>'elementor_meta_generator_tag', 'allowed'=>['1','']],
'unfiltered_files_upload' => ['key'=>'elementor_unfiltered_files_upload', 'allowed'=>['1','']],
'font_display' => ['key'=>'elementor_font_display', 'allowed'=>['auto','block','swap','fallback','optional']],
'google_maps_api_key' => ['key'=>'elementor_google_maps_api_key', 'allowed'=>null],
'element_cache_ttl' => ['key'=>'elementor_element_cache_ttl', 'allowed'=>null,'type'=>'int'],
'optimized_gutenberg_loading' => ['key'=>'elementor_optimized_gutenberg_loading', 'allowed'=>['1','']],
];
$updated = []; $errors = []; $updated = []; $errors = [];
foreach ($settings as $field => $value) { foreach ($settings as $field => $value) {
if (!isset($map[$field])) { $errors[] = "Unknown field: {$field}"; continue; } if (!isset($map[$field])) { $errors[] = "Unknown field: {$field}"; continue; }
$def = $map[$field]; $def = $map[$field];
if (!empty($def['allowed']) && !in_array((string)$value, $def['allowed'], true)) { if (!empty($def['allowed']) && !in_array((string)$value,$def['allowed'],true)) { $errors[] = "Invalid value for {$field}: {$value}."; continue; }
$errors[] = "Invalid value for {$field}: {$value}. Allowed: " . implode(', ', $def['allowed']); continue;
}
$write_val = isset($def['type']) && $def['type']==='int' ? (int)$value : (string)$value; $write_val = isset($def['type']) && $def['type']==='int' ? (int)$value : (string)$value;
update_option($def['key'], $write_val); update_option($def['key'], $write_val);
$updated[$field] = $write_val; $updated[$field] = $write_val;
@@ -615,15 +619,8 @@ class ElementorTools {
return ['success'=>empty($errors),'updated'=>$updated,'errors'=>$errors]; return ['success'=>empty($errors),'updated'=>$updated,'errors'=>$errors];
} }
// =========================================================
// CPT SUPPORT
// =========================================================
public function getCptSupport(array $args): array { public function getCptSupport(array $args): array {
return [ return ['supported_post_types'=>(array)(get_option('elementor_cpt_support')?:['post','page']),'available_post_types'=>array_keys(get_post_types(['public'=>true],'names'))];
'supported_post_types' => (array)(get_option('elementor_cpt_support') ?: ['post','page']),
'available_post_types' => array_keys(get_post_types(['public'=>true], 'names')),
];
} }
public function setCptSupport(array $args): array { public function setCptSupport(array $args): array {
@@ -636,20 +633,10 @@ class ElementorTools {
return ['success'=>true,'supported_post_types'=>$post_types]; return ['success'=>true,'supported_post_types'=>$post_types];
} }
// =========================================================
// MAINTENANCE MODE
// =========================================================
public function getMaintenanceMode(array $args): array { public function getMaintenanceMode(array $args): array {
$mode = get_option('elementor_maintenance_mode_mode',''); $mode = get_option('elementor_maintenance_mode_mode','');
$page_id = (int)get_option('elementor_maintenance_mode_page_id',0); $page_id = (int)get_option('elementor_maintenance_mode_page_id',0);
return [ return ['mode'=>$mode?:'disabled','page_id'=>$page_id,'page_url'=>$page_id?get_permalink($page_id):'','exclude_mode'=>get_option('elementor_maintenance_mode_exclude_mode',''),'is_active'=>!empty($mode)];
'mode' => $mode ?: 'disabled',
'page_id' => $page_id,
'page_url' => $page_id ? get_permalink($page_id) : '',
'exclude_mode' => get_option('elementor_maintenance_mode_exclude_mode', ''),
'is_active' => !empty($mode),
];
} }
public function setMaintenanceMode(array $args): array { public function setMaintenanceMode(array $args): array {
@@ -663,36 +650,14 @@ class ElementorTools {
return ['success'=>true,'mode'=>$mode?:'disabled','page_id'=>$page_id?:(int)get_option('elementor_maintenance_mode_page_id',0),'exclude_mode'=>$exclude_mode?:get_option('elementor_maintenance_mode_exclude_mode','')]; return ['success'=>true,'mode'=>$mode?:'disabled','page_id'=>$page_id?:(int)get_option('elementor_maintenance_mode_page_id',0),'exclude_mode'=>$exclude_mode?:get_option('elementor_maintenance_mode_exclude_mode','')];
} }
// =========================================================
// SITE SETTINGS
// =========================================================
public function getSiteSettings(array $args): array { public function getSiteSettings(array $args): array {
return [ return ['google_maps_api_key'=>get_option('elementor_google_maps_api_key',''),'meta_generator_tag'=>get_option('elementor_meta_generator_tag',''),'google_font'=>get_option('elementor_google_font','1'),'font_display'=>get_option('elementor_font_display','auto'),'load_fa4_shim'=>get_option('elementor_load_fa4_shim',''),'unfiltered_files_upload'=>get_option('elementor_unfiltered_files_upload',''),'css_print_method'=>get_option('elementor_css_print_method','external'),'elementor_version'=>get_option('elementor_version',''),'pro_version'=>get_option('elementor_pro_version','')];
'google_maps_api_key' => get_option('elementor_google_maps_api_key', ''),
'meta_generator_tag' => get_option('elementor_meta_generator_tag', ''),
'google_font' => get_option('elementor_google_font', '1'),
'font_display' => get_option('elementor_font_display', 'auto'),
'load_fa4_shim' => get_option('elementor_load_fa4_shim', ''),
'unfiltered_files_upload' => get_option('elementor_unfiltered_files_upload', ''),
'css_print_method' => get_option('elementor_css_print_method', 'external'),
'elementor_version' => get_option('elementor_version', ''),
'pro_version' => get_option('elementor_pro_version', ''),
];
} }
public function updateSiteSettings(array $args): array { public function updateSiteSettings(array $args): array {
$settings = $args['settings'] ?? []; $settings = $args['settings'] ?? [];
if (empty($settings)) throw new \RuntimeException('settings object is required.'); if (empty($settings)) throw new \RuntimeException('settings object is required.');
$allowed = [ $allowed = ['google_maps_api_key'=>'elementor_google_maps_api_key','meta_generator_tag'=>'elementor_meta_generator_tag','google_font'=>'elementor_google_font','font_display'=>'elementor_font_display','load_fa4_shim'=>'elementor_load_fa4_shim','unfiltered_files_upload'=>'elementor_unfiltered_files_upload','css_print_method'=>'elementor_css_print_method'];
'google_maps_api_key' => 'elementor_google_maps_api_key',
'meta_generator_tag' => 'elementor_meta_generator_tag',
'google_font' => 'elementor_google_font',
'font_display' => 'elementor_font_display',
'load_fa4_shim' => 'elementor_load_fa4_shim',
'unfiltered_files_upload' => 'elementor_unfiltered_files_upload',
'css_print_method' => 'elementor_css_print_method',
];
$updated = []; $errors = []; $updated = []; $errors = [];
foreach ($settings as $field => $value) { foreach ($settings as $field => $value) {
if (!isset($allowed[$field])) { $errors[] = "Unknown or read-only field: {$field}"; continue; } if (!isset($allowed[$field])) { $errors[] = "Unknown or read-only field: {$field}"; continue; }
@@ -706,15 +671,21 @@ class ElementorTools {
// PRIVATE HELPERS // PRIVATE HELPERS
// ========================================================= // =========================================================
private function buildOutline(array $elements, int $depth = 0): array { /**
$result = []; * Inserts $element immediately after $target_id in the tree (by-reference).
foreach ($elements as $el) { * Mirrors insert_after() from Elementor_MCP_Layout_Abilities v1.9.0.
$entry = ['id'=>$el['id']??'','type'=>$el['elType']??'','depth'=>$depth]; */
if (($el['elType']??'') === 'widget') $entry['widget_type'] = $el['widgetType']??''; private function insertAfter(array &$data, string $target_id, array $element): bool {
if (!empty($el['elements'])) $entry['children'] = $this->buildOutline($el['elements'], $depth+1); foreach ($data as $index => &$item) {
$result[] = $entry; if (isset($item['id']) && $item['id'] === $target_id) {
array_splice($data, $index+1, 0, [$element]);
return true;
} }
return $result; if (!empty($item['elements']) && is_array($item['elements'])) {
if ($this->insertAfter($item['elements'], $target_id, $element)) return true;
}
}
return false;
} }
private function insertIntoParent(array $elements, string $parent_id, array $element, int $position): array { private function insertIntoParent(array $elements, string $parent_id, array $element, int $position): array {
@@ -733,6 +704,17 @@ class ElementorTools {
return $elements; return $elements;
} }
private function buildOutline(array $elements, int $depth = 0): array {
$result = [];
foreach ($elements as $el) {
$entry = ['id'=>$el['id']??'','type'=>$el['elType']??'','depth'=>$depth];
if (($el['elType']??'') === 'widget') $entry['widget_type'] = $el['widgetType']??'';
if (!empty($el['elements'])) $entry['children'] = $this->buildOutline($el['elements'], $depth+1);
$result[] = $entry;
}
return $result;
}
private function getActiveKit(): ?\WP_Post { private function getActiveKit(): ?\WP_Post {
if (!class_exists('\Elementor\Plugin')) return null; if (!class_exists('\Elementor\Plugin')) return null;
$kit_id = get_option('elementor_active_kit'); $kit_id = get_option('elementor_active_kit');